import random

# python_demo.py - demonstration of CS1 Python basics
# Author: Todd W. Neller

# Simple input/output

# Enter (without comment #'s):
# hello
# 42
# 3.14159265358

# Or redirect demo-input.txt to the standard input from the command line with the command
# python python_demo.py < demo-input.txt

line = input()  # Read string line
n = int(input())  # Read integer
x = float(input())  # Read floating-point number
print(n, end='')  # Print integer without newline
print(line)  # Print hello with newline
print(f'{x:.2f}')  # Formatted print floating point number to 2 decimal places

# Arithmetic expressions
num = 13
den = 5
print(f'{num} / {den} = {num // den} remainder {num % den}')
celsius = 30.0
fahrenheit = celsius * 9.0 / 5.0 + 32.0
print(f'{celsius:.6f} Celsius = {fahrenheit:.6f} Fahrenheit')
fahrenheit = 100.0
celsius = (fahrenheit - 32.0) * 5.0 / 9.0
print(f'{fahrenheit:.6f} Fahrenheit = {celsius:.6f} Celsius')
print(f'{den} cubed = {den ** 3}')

# Random numbers and decisions / selections
x = random.random() - 0.5  # Random floating point number in [0, 1) - 0.5
# if-else expression
print(x, 'is', 'positive.' if x > 0 else 'nonpositive.')
# Random integer in [min, max]
min_int, max_int = -1, 1
n = random.randint(min_int, max_int)
print(n, 'is ', end='')
# if-else chain
if n > 0:
    print('positive.')  # Note: Indentation defines block structure
elif n == 0:
    print('zero.')
else:  # n < 0
    print('negative.')

# String manipulation
s = 'testing 123'
# Getting a substring
print(s[:7])  # Substring beginning
print(s[2:7])  # Substring internal
print(s[8:])  # Substring end
print('string' + ' ' + 'concatenation')  # Concatenation
print('Number from string:', int(s[8:]))
first = s.index('t')  # Find index of first "t"
last = s.rindex('t')  # Find index of last "t"
ch = s[first]  # Get single character "t"
print(f'Character "{ch}" is at indices {first} and {last}.')

# Arrays / Lists, Loops / Iteration, Sorting

# 10 random ints
n = 10
arr = [None] * n  # List of length n (initialized to None)
for i in range(len(arr)):  # for loop
    arr[i] = random.randint(0, n - 1)
print(arr)
# Note: arr is not fixed length, as with a tuple.  We merely preallocate it
arr2 = [i for i in arr]  # List grown dynamically through list comprehension
# Sorting
arr = sorted(arr)
arr2 = sorted(arr2)
print('Sorted:', arr2)
# (while loop in binary_search function below)

# Functions


def binary_search(array, target):
    """Perform a binary search on a given sorted integer array
     for a given target value, returning the index of the target value
     or -1 if no such target value is found."""
    low_idx, high_idx = 0, len(array) - 1
    while low_idx <= high_idx:
        mid_idx = (low_idx + high_idx) // 2
        value = array[mid_idx]
        if value == target:
            return mid_idx
        elif value > target:
            high_idx = mid_idx - 1
        else:  # value < target
            low_idx = mid_idx + 1
    return -1  # index indicating search failure


print('value -> index found (-1 = "not found")')
for i in range(n):
    print(f'{i} -> {binary_search(arr, i)}')

# Objects, Recursion


class Node:  # Node class with recursive depth-first search

    def __init__(self, label):
        self.label = label
        self.children = []

    def depth_first_search(self, target):
        if self is target:
            return [self]
        for child in self.children:
            reverse_path = child.depth_first_search(target)
            if reverse_path:
                reverse_path.append(self)
                return reverse_path
        return None

    def __str__(self):
        return self.label


# Create random tree of nodes
num_nodes = 10
nodes = [Node(chr(ord('A') + i)) for i in range(num_nodes)]
random.shuffle(nodes)   # Shuffle node order
for i in range(1, num_nodes):
    # Add each node to the children of a random predecessor in the order
    parent = nodes[random.randint(0, i - 1)]
    child = nodes[i]
    parent.children.append(child)

# Display the children of each non-leaf node
print('Random tree:')
for node in nodes:
    if node.children:
        print(node, '-->', end='')
        for child in node.children:
            print('', child, end='')
        print()

# Perform a recursive depth-first search of the tree to find and display
#   the path from the first node to the last node of the order.
root = nodes[0]
target = nodes[num_nodes - 1]
reverse_path = root.depth_first_search(target)
print(f'Path from {root} to {target}:', end='')
if reverse_path:
    for i in range(len(reverse_path) - 1, -1, -1):
        print('', reverse_path[i], end='')
    print()
else:
    print(' (none)')

# Maps / Hashtables / Dictionaries

# Count the frequencies of values in previous random data
import collections
unsorted_freq_dict = dict(collections.Counter(arr))  # unsorted frequency dictionary
freq_dict = dict(sorted(unsorted_freq_dict.items()))  # sorted frequency dictionary
# Print the frequencies of values in ascending order of value
print('Count\tValue')
for key in freq_dict.keys():
    print(f'{key}\t{freq_dict[key]}')

# Sets

missing_values = set(range(n))
missing_values -= freq_dict.keys()
print(f'Missing values in range 0-{n-1}:', end='')
for value in missing_values:
    print(f' {value}', end='')
print()

# Queues, Stacks

# Put 0 through n - 1 into the queue.
queue = collections.deque()
for i in range(n):
    queue.appendleft(i)
# Remove and print numbers first-in, first-out (FIFO), comma-separated
for i in range(n):
    print(str(queue.pop()) + (', ' if i < n - 1 else '\n'), end='')

# Push 0 through n - 1 onto the stack.
stack = collections.deque()
for i in range(n):
    stack.append(i)
# Pop and print numbers last-in, first-out (LIFO), comma-separated
for i in range(n):
    print(str(stack.pop()) + (', ' if i < n - 1 else '\n'), end='')

