## Backtracking

1.   Permutation
2.   Combination
3.    All Paths

### Template

```
def backtrack():
  # initialization
  A #a working data structure, either a list of candidates or a graph or a matrix representing a board
  state_tracker = []*n
  assist_state_tracker
  # main backtracking
  def dfs(d, n):
    '''d: depth representing level in the tree'''
    if d == n:
      return
    candidates = generate_candidates(state_tracker, assist_state_tracker)
    for c in candidates:
      set_state(state_tracker, assist_state_tracker, c)
      dfs(d+1, n)
      reset_state(state_tracler, assist_state_tracker, c)

  dfs(0, n)

```

### Permutations
How many ways to permutate an $n$-set? For example {1, 2, 3}: ???
* Enumerate by position This helps us to enumerate permuation with **backtracking**
```
{}
i=0: {1}, {2}, {3}
i=1: {1, 2}, {1, 3}, {2, 1}, {2, 3}, {3, 1}, {3,2}
i=2: {1, 2, 3}, {1, 3, 2}, {2, 1, 3}, {2, 3, 1}, {3, 1, 2}, {3, 2, 1}.
```
The recurrence relation is:
\begin{align}
d(i) = (n-i)*d(i-1)
\end{align}
Such that
```
d(0) = 3 * 1 = 3
d(1) = 2 * 3 = 6
d(2) = 1 * 6 = 6
```
*  Additionally, can enumerate by iterating elements, and then enumerate all possible positions that it can go.
```
: {}
1: {1}
2: {1, 2}, {2, 1} : can find i position to insert 2
3: {3, 1, 2}, {1, 3, 2}, {1, 2, 3}| {3, 2, 1}, {2, 3, 1}, {2, 1, 3}
```
\begin{align}
d(i) = i*d(i-1)
\end{align}
Such that:
```
d(0) = 1 * 1 = 1
d(1) = 2 * 1 = 2
d(2) = 3 * 2 = 6
```

```
from graphviz import Digraph
dot = Digraph(comment='The Round Table', format='png')
dot.node_attr['shape']='box'
#print(get_methods(Digraph))
#print(Digraph.__dict__)
nodes = ['[]', '[1]', '[2]', '[3]', '[1, 2]', '[1, 3]', '[2, 1]', '[2, 3]', '[3, 1]', '[3, 2]', '[1, 2, 3]', '[1, 3, 2]', '[2, 1, 3]', '[2, 3, 1]', '[3, 1, 2]', '[3, 2, 1]']
for i, node in enumerate(nodes):
  dot.node(str(i), label=node)
edges = [('0', '1'), ('0', '2'), ('0', '3'), ('1', '4'), ('1', '5'), ('2', '6'), ('2', '7'), ('3', '8'), ('3', '9'), ('4', '10'), ('5', '11'), ('6', '12'), ('7', '13'), ('8', '14'), ('9', '15')]
for n1, n2 in edges:
  dot.edge(n1, n2)
# dot.edge('0', '1', _attributes={'label': '4'})
# dot.edge('S', 'B', _attributes={'label': '5'})
# dot.edge('A', 'G', _attributes={'label': '7'})
# dot.edge('B', 'G', _attributes={'label': '3'})
dot.render('test-output/permutation', view=True)

dot
```

```
def p_n_m(a, n, m, d, used, curr, ans):
  print(curr, end='->')
  if d == m: #end condition
    ans.append(curr[::])
    return

  for i in range(n):
    if not used[i]:
      # generate the next solution from curr
      curr.append(a[i])
      used[i] = True

      # move to the next solution
      p_n_m(a, n, m, d + 1, used, curr, ans)
      #backtrack to previous partial state
      curr.pop()
      print('backtrack:', curr)
      used[i] = False
  return
```

```
a = [1, 2, 3]
n = len(a)
ans = [[None]]
used = [False] * len(a)
ans = []
p_n_m(a, n, n, 0, used, [], ans)
print(ans)
```

    []->[1]->[1, 2]->[1, 2, 3]->backtrack: [1, 2]
    backtrack: [1]
    [1, 3]->[1, 3, 2]->backtrack: [1, 3]
    backtrack: [1]
    backtrack: []
    [2]->[2, 1]->[2, 1, 3]->backtrack: [2, 1]
    backtrack: [2]
    [2, 3]->[2, 3, 1]->backtrack: [2, 3]
    backtrack: [2]
    backtrack: []
    [3]->[3, 1]->[3, 1, 2]->backtrack: [3, 1]
    backtrack: [3]
    [3, 2]->[3, 2, 1]->backtrack: [3, 2]
    backtrack: [3]
    backtrack: []
    [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

###Swapping Method

Extention: Johnson-Trotter algorithm,
* https://en.wikipedia.org/wiki/Steinhaus%E2%80%93Johnson%E2%80%93Trotter_algorithm
*

```
from graphviz import Digraph
dot = Digraph(comment='The Round Table', format='png')
dot.node_attr['shape']='box'
#print(get_methods(Digraph))
#print(Digraph.__dict__)
nodes = ['[1, 2, 3]', '[1, 2, 3]', '[2, 1, 3]', '[3, 2, 1]', '[1, 2, 3]', '[1, 3, 2]', '[2, 1, 3]', '[2, 3, 1]', '[3, 2, 1]', '[3, 1, 2]',
         '[1, 2, 3]', '[1, 3, 2]', '[2, 1, 3]', '[2, 3, 1]', '[3, 2, 1]', '[3, 1, 2]']
for i, node in enumerate(nodes):
  dot.node(str(i), label=node)
edges = [('0', '1', '(0, 0)'), ('0', '2', '(0, 1)'), ('0', '3', '(0, 2)'), ('1', '4', '(1, 1)'), ('1', '5', '(1, 2)'), ('2', '6', '(1, 1)'), ('2', '7', '(1, 2)'),
         ('3', '8', '(1, 1)'), ('3', '9', '(1, 2)'), ('4', '10', '(2, 2)'), ('5', '11', '(2, 2)'), ('6', '12', '(2, 2)'), ('7', '13', '(2, 2)'),
         ('8', '14', '(2, 2)'), ('9', '15', '(2, 2)')]
for n1, n2, l in edges:
  dot.edge(n1, n2,  _attributes={'label': l})
# dot.edge('0', '1', _attributes={'label': '4'})
# dot.edge('S', 'B', _attributes={'label': '5'})
# dot.edge('A', 'G', _attributes={'label': '7'})
# dot.edge('B', 'G', _attributes={'label': '3'})
dot.render('test-output/permutation_swap', view=True)

dot
```

```
# permutation by swapping
ans = []
def permutate(a, d):
  global ans

  if d == len(a):
    ans.append(a[::])
  for i in range(d, len(a)):
    a[i], a[d] = a[d], a[i]
    print(a, '(', d, i, ')')
    permutate(a, d+1)
    a[i], a[d] = a[d], a[i]
  return
```

```
a = [1, 2, 3]
permutate(a, 0)
ans
```

    [1, 2, 3] ( 0 0 )
    [1, 2, 3] ( 1 1 )
    [1, 2, 3] ( 2 2 )
    [1, 3, 2] ( 1 2 )
    [1, 3, 2] ( 2 2 )
    [2, 1, 3] ( 0 1 )
    [2, 1, 3] ( 1 1 )
    [2, 1, 3] ( 2 2 )
    [2, 3, 1] ( 1 2 )
    [2, 3, 1] ( 2 2 )
    [3, 2, 1] ( 0 2 )
    [3, 2, 1] ( 1 1 )
    [3, 2, 1] ( 2 2 )
    [3, 1, 2] ( 1 2 )
    [3, 1, 2] ( 2 2 )

    [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]]

When there are duplicates, for example,
https://www.geeksforgeeks.org/distinct-permutations-string-set-2/

```
# permutation by swapping and there might be having duplicates
ans = []
def checkSwap(a, d, cur):
  for i in range(d, cur):
    if a[i] == a[cur]:
      return False
  return True
def permutate(a, d):
  global ans
  if d == len(a):
    ans.append(a[::])
  for i in range(d, len(a)):
    if not checkSwap(a, d, i):
      continue
    a[i], a[d] = a[d], a[i]
    permutate(a, d+1)
    a[i], a[d] = a[d], a[i]
  return
```

```
a = [1,2, 2, 3]
permutate(a, 0)
ans
```

    [[1, 2, 2, 3],
     [1, 2, 3, 2],
     [1, 3, 2, 2],
     [2, 1, 2, 3],
     [2, 1, 3, 2],
     [2, 2, 1, 3],
     [2, 2, 3, 1],
     [2, 3, 2, 1],
     [2, 3, 1, 2],
     [3, 2, 2, 1],
     [3, 2, 1, 2],
     [3, 1, 2, 2]]

#### Questions to ponder
1. What if there are duplicates in the input list? How to count and how to enumerate all possible permutations? [1, 2, 3, 2]

We have a way to count:
\begin{align}
\frac{P(n, n)}{c_1!c_2!...}
\end{align}

Resources
* https://www.cs.sfu.ca/~ggbaker/zju/math/perm-comb-more.html

[47. Permutations II](https://leetcode.com/problems/permutations-ii/)

* first sort it as [1, 2, 2, 3]. We can draw the process
```
{}
i=0: {1}, {2}, *{2}*,  {3} = 3
i=1: {1, 2}, *{1, 2}*,  {1, 3}, {2, 1}, {2, 2}, {2, 3}, {3, 1}, {3,2}, *{3, 2}*
i=2: {1, 2, 2}, {1, 2, 3},
```
Exactlyt the same process except that we skip the duplicates.
We can not find a recurrence relation to this. This depends on how many duplicates we have.

```
from graphviz import Digraph
dot = Digraph(comment='The Round Table', format='png')
dot.node_attr['shape']='box'
#print(get_methods(Digraph))
#print(Digraph.__dict__)
nodes = [('0', '[]'), ('1', '[1]'), ('2', '[2]'), ('3', '[2]'), ('4', '[3]'),
         ('5', '[1, 2]'), ('6', '[1, 2]'), ('7', '[1, 3]'),
         ('8', '[2, 1]'), ('9', '[2, 2]'), ('10', '[2, 3]'),
         ('11', '[3, 1]'), ('12', '[3, 2]'), ('12_1', '[3, 2]'),
         ('13', '[1, 2, 2]'), ('14', '[1, 2, 3]'),
         ('15', '[1, 3, 2]'),
         ('16', '[2, 1, 2]'), ('17', '[2, 1, 3]'),
         ('18', '[2, 2, 1]'), ('19', '[2, 2, 3]'),
         ('20', '[2, 3, 1]'), ('21', '[2, 3, 2]'),
         ('22', '[3, 1, 2]'), ('22_1', '[3, 1, 2]') ,
         ('23', '[3, 2, 1]'), ('24', '[3, 2, 2]'),
         ('25', '[1, 2, 2, 3]'),
         ('26', '[1, 2, 3, 2]'),
         ('27', '[1, 3, 2, 2]'),
         ('28', '[2, 1, 2, 3]'),
         ('29', '[2, 1, 3, 2]'),
         ('30', '[2, 2, 1, 3]'),
         ('31', '[2, 2, 3, 1]'),
         ('32', '[2, 3, 1, 2]'),
         ('33', '[2, 3, 2, 1]'),
         ('34', '[3, 1, 2, 2]'),
         ('35', '[3, 2, 1, 2]'),
         ('36', '[3, 2, 2, 1]')]
for i, node in nodes:
  dot.node(i, label=node)
edges = [('0', '1'), ('0', '2'), ('0', '3'), ('0', '4'),
         ('1', '5'), ('1', '6'), ('1', '7'),
         ('2', '8'), ('2', '9'), ('2', '10'),
         ('4', '11'), ('4', '12'),('4', '12_1'),
         ('5', '13'), ('5', '14'),
         ('7', '15'),
         ('8', '16'), ('8', '17'),
         ('9', '18'), ('9', '19'),
         ('10', '20'), ('10', '21'),
         ('11', '22'), ('11', '22_1'),
         ('12', '23'),('12', '24'),
         ('13', '25'),('14', '26'), ('15', '27'),('16', '28'), ('17', '29'),('18', '30'),
         ('19', '31'),('20', '32'), ('21', '33'),('22', '34'), ('23', '35'),('24', '36')]
for n1, n2 in edges:
  dot.edge(n1, n2)
# dot.edge('0', '1', _attributes={'label': '4'})
# dot.edge('S', 'B', _attributes={'label': '5'})
# dot.edge('A', 'G', _attributes={'label': '7'})
# dot.edge('B', 'G', _attributes={'label': '3'})
dot.render('test-output/permutation_repeat', view=True)

dot
```

```
from collections import Counter
def permuteDup(nums, k):
    ans = []
    def permutate(d, n, k, curr, tracker):
      nonlocal ans
      if d == k:
          ans.append(curr)
          return
      for i in range(n):
          if tracker[nums[i]] == 0:
            #print('continue')
            continue
          if i - 1 >= 0 and nums[i] == nums[i-1]:
              continue
          tracker[nums[i]] -= 1
          curr.append(nums[i])
          #print(curr)
          permutate(d+1, n, k, curr[:], tracker)
          curr.pop()
          tracker[nums[i]] += 1
      return

    nums.sort()
    permutate(0, len(nums), k, [], Counter(nums))
    return ans

```

```
nums = [1,2, 2, 3]
ans = permuteDup(nums, 4)
print(len(ans), ans)
```

    12 [[1, 2, 2, 3], [1, 2, 3, 2], [1, 3, 2, 2], [2, 1, 2, 3], [2, 1, 3, 2], [2, 2, 1, 3], [2, 2, 3, 1], [2, 3, 1, 2], [2, 3, 2, 1], [3, 1, 2, 2], [3, 2, 1, 2], [3, 2, 2, 1]]

### Combination

```
from graphviz import Digraph
dot = Digraph(comment='The Round Table', format='png')
dot.node_attr['shape']='box'
#print(get_methods(Digraph))
#print(Digraph.__dict__)
nodes = ['{}', '{1}', '{2}', '{3}', '{1, 2}', '{1, 3}',  '{2, 3}',  '{1, 2, 3}']
for i, node in enumerate(nodes):
  dot.node(str(i), label=node)
edges = [('0', '1'), ('0', '2'), ('0', '3'), ('1', '4'), ('1', '5'), ('2', '6'), ('4', '7')]
for n1, n2 in edges:
  dot.edge(n1, n2)
dot.render('test-output/combination', view=True)
dot
```

```
def C_n_k(a, n, k, start, d, curr, ans):
  '''
  Implement combination of k items out  of n items
  start: the start of candinate
  depth: start from 0, and represent the depth of the search
  curr: the current partial solution
  ans: collect all the valide solutions
  '''
  if d == k: #end condition
    ans.append(curr[::])
    return

  for i in range(start, n):
    # generate the next solution from curr
    curr.append(a[i])
    # move to the next solution
    C_n_k(a, n, k, i+1, d+1, curr, ans)

    #backtrack to previous partial state
    curr.pop()
  return
```

```
a = [1, 2, 3]
n = len(a)
ans = []
a.sort()
C_n_k(a, n, 2, 0, 0, [], ans)
print(ans, a)
```

    [[1, 2], [1, 3], [2, 3]] [1, 2, 3]

```
def powerset(a, n, d, curr, ans):
  if d == n:
    ans.append(curr[::])
    return

  # Case 1: select item
  curr.append(a[d])
  powerset(a, n, d + 1, curr, ans)
  # Case 2: not select item
  curr.pop()
  powerset(a, n, d + 1, curr, ans)
  return
```

```
a = [1, 2, 3]
n = len(a)
ans = []
powerset(a, n, 0, [], ans)
ans
```

    [[1, 2, 3], [1, 2], [1, 3], [1], [2, 3], [2], [3], []]

#### Questions to ponder
1. What if there are duplicates in the input list? How to count and how to enumerate all possible combinations? [1, 2, 3, 2]

Take the product of all the (frequencies + 1).

For example, in {A,B,B}, the answer is (1+1) [the number of As] * (2+1) [the number of Bs] = 6.

In the second example, count(A) = 2 and count(B) = 2. Thus the answer is (2+1) * (2+1) = 9.

The reason this works is that you can define any subset as a vector of counts - for {A,B,B}, the subsets can be described as {A=0,B=0}, {A=0,B=1}, {0,2}, {1,0}, {1,1}, {1,2}.

For each number in counts[] there are (frequencies of that object + 1) possible values. (0..frequencies)

Therefore, the total number of possiblities is the product of all (frequencies+1).

The "all unique" case can also be explained this way - there is one occurence of each object, so the answer is (1+1)^|S| = 2^|S|.

However, how to count the case of $c(n, k)$? Assume we have $m$ unqiue items, and the frequency of each is marked as $x_i$, with $\sum_{i=0}^{m-1}x_i = n$.
\begin{align}
\sum_{k=0}^{n} c(n, k) = \prod_{i=0}^{m-1}(x_i + 1)
\end{align}

When the maximum of $k$ is 0.
\begin{align}
\sum_{k=0}^{0} c(n, k) = \prod_{i=0}^{m-1}(1) = 1
\end{align}

When the maximum of $k$ is 1.
\begin{align}
\sum_{k=0}^{1} c(n, k) = \prod_{i=0}^{m-1}(1) = 1
\end{align}

We list each as follows:
```
c(n,0)
```

Notes:
* http://www.math.ucsd.edu/~ebender/CombText/ch-1.pdf
* https://math.stackexchange.com/questions/1506536/counting-tuples-with-repetitions

```
def C_n_k(a, n, k, start, depth, curr, ans):
  '''
  Implement combination of k items out  of n items
  start: the start of candinate
  depth: start from 0, and represent the depth of the search
  curr: the current partial solution
  ans: collect all the valide solutions
  '''
  ans.append(curr[::])
  if depth == k: #end condition
    return

  for i in range(start, n):
    if i - 1 >= start and a[i] == a[i-1]:
              continue
    # generate the next solution from curr
    curr.append(a[i])
    # move to the next solution
    C_n_k(a, n, k, i+1, depth+1, curr, ans)
    curr.pop()
  return
```

```
a = [1, 2, 3, 2]
n = len(a)
ans = [[None]]
ans = []
a.sort()
C_n_k(a, n, 4, 0, 0, [], ans)
ans, len(ans)
```

    ([[],
      [1],
      [1, 2],
      [1, 2, 2],
      [1, 2, 2, 3],
      [1, 2, 3],
      [1, 3],
      [2],
      [2, 2],
      [2, 2, 3],
      [2, 3],
      [3]],
     12)

### More Combinatorics

#### All paths

```
def all_paths(g, s, path, ans):
  '''generate all pahts with backtrack'''
  ans.append(path[::])
  for v in g[s]:
    path.append(v)
    print(path)
    all_paths(g, v, path, ans)
    path.pop()
    print(path, 'backtrack')
```

```
al = [[1, 2], [2, 3, 4], [5], [], [], []]
print(al)
```

    [[1, 2], [2, 3, 4], [5], [], [], []]

```
dot = Digraph(comment='The Round Table', format='png')
nodes = range(len(al))
for idx, neighbors in enumerate(al):
  for n in neighbors:
      dot.edge(str(idx), str(n))
rank1 = [0]
rank2 = [1, 2]
rank3 = [3, 4, 5]
for rank in [rank1, rank2, rank3]:
  with dot.subgraph() as s:
    s.attr(rank='same')
    for node in rank:
      s.node(str(node))
dot.render('test-output/all_path_demo', view=True)
dot
```

```
ans = []
path = [0]
all_paths(al, 0, path, ans)
for path in ans:
  path = [str(i) for i in path]
  print('->'.join(path), end = ', ')
```

    [0, 1]
    [0, 1, 2]
    [0, 1, 2, 5]
    [0, 1, 2] backtrack
    [0, 1] backtrack
    [0, 1, 3]
    [0, 1] backtrack
    [0, 1, 4]
    [0, 1] backtrack
    [0] backtrack
    [0, 2]
    [0, 2, 5]
    [0, 2] backtrack
    [0] backtrack
    0, 0->1, 0->1->2, 0->1->2->5, 0->1->3, 0->1->4, 0->2, 0->2->5,

#### Subsequences

* Sequence is unique
* String has repetition: 940. Distinct Subsequences II

The enumeration with backtacking is quite similar to the combination, other than in the case with repetition. In our previous implementation of enumerating unique combinations when there are duplciates in the input, we have sorted the items, making the checking of repetition of choices as simple as checking the precessor.

```
#Counting
 def distinctSubseqII(S):
    dp = [1]
    last = {}
    for i, x in enumerate(S):
        dp.append(dp[-1] * 2)
        if x in last:
            dp[-1] -= dp[last[x]]
        last[x] = i

    return (dp[-1] - 1) % (10**9 + 7)
```

```
S = 'abaab'
distinctSubseqII(S)
```

    17

```
#Enumerating
def check_repetition(start, i, a):
  for j in range(start, i):
    if a[i] == a[j]:
      return True
  return False

def subseqs(a, n, start, curr, ans):
  ans.append(''.join(curr[::]))
  if start == n:
    return

  for i in range(start, n):
    if check_repetition(start, i, a):
      continue
    curr.append(a[i])
    subseqs(a, n, i+1, curr, ans)
    curr.pop()
  return
```

```
from graphviz import Graph
dot = Graph(comment='The Round Table', format='png')
dot.node_attr['shape']='ellipse'
dot.node('0', label='<\'\''+'<FONT POINT-SIZE="12">, s={}</FONT>>'.format(0))
count = 0

def subseqs(a, n, start, curr, ans, node_label):
  global count
  ans.append(''.join(curr[::]))
  if start == n:
    return

  for i in range(start, n):
    node = curr + [a[i]]
    count += 1
    dot.node(str(count), label='<\''+''.join(node)+'\''+'<FONT POINT-SIZE="12">, s={}</FONT>>'.format(i+1))
    dot.edge(node_label, str(count), _attributes={ 'label':'i={}'.format(i)})
    if check_repetition(start, i, a):
      dot.node(str(count), label='\''+''.join(node)+'\'', _attributes={'color': 'red'})
      continue
    curr.append(a[i])
    subseqs(a, n, i+1, curr, ans, str(count))
    curr.pop()
    #count -= 1
  return
```

```
S = '1232'
ans = []
subseqs(list(S), len(S), 0,  [], ans, '0')
print(len(ans), ans)
dot.render('test-output/subsequence', view=True)
dot
```

    14 ['', '1', '12', '123', '1232', '122', '13', '132', '2', '23', '232', '22', '3', '32']

#### Partition

## Constraint Satisfaction Problems with Backtracking and Pruning

### Sudoku Solver
[Search space](https://www.researchgate.net/publication/264572573_Sudoku_Puzzle_Complexity)

First, we build up the board

```
board = [[5, 3, None, None, 7, None, None, None, None],
         [6, None, None, 1, 9, 5, None, None, None],
         [None, 9, 8, None, None, None, None, 6, None],
         [8, None, None, None, 6, None, None, None, 3],
         [4, None, None, 8, None, 3, None, None, 1],
         [7, None, None, None, 2, None, None, None, 6],
         [None, 6, None, None, None, None, 2, 8, None],
         [None, None, None, 4, 1, 9, None, None, 5],
         [None, None, None, None, 8, None, None, 7, 9]]
```

Define how to change the state

```
def setState(i, j, v, row_state, col_state, grid_state):
  row_state[i] |= 1 << v
  col_state[j] |= 1 << v
  grid_index = (i//3)*3 + (j//3)
  grid_state[grid_index] |= 1 << v

def resetState(i, j, v, row_state, col_state, grid_state):
  row_state[i] &= ~(1 << v)
  col_state[j] &= ~(1 << v)
  grid_index = (i//3)*3 + (j//3)
  grid_state[grid_index] &= ~(1 << v)

def checkState(i, j, v, row_state, col_state, grid_state):
  row_bit = (1 << v) & row_state[i] != 0
  col_bit = (1 << v) & col_state[j]  != 0
  grid_index = (i//3)*3 + (j//3)
  grid_bit = (1 << v) & grid_state[grid_index]  != 0
  return not row_bit and not col_bit and not grid_bit
```

Get the empty spots and its values

```
 def getEmptySpots(board, rows, cols, row_state, col_state, grid_state):
    ''' get empty spots and find its corresponding values in O(n*n)'''
    empty_spots = {}
    # initialize the state, and get empty spots
    for i in range(rows):
      for j in range(cols):
        if board[i][j]:
            # set that bit to 1
            setState(i, j, board[i][j]-1, row_state, col_state, grid_state)
        else:
            empty_spots[(i,j)] = []

    # get possible values for each spot
    for i, j in empty_spots.keys():
      for v in range(9):
        if checkState(i, j, v, row_state, col_state, grid_state):
          empty_spots[(i, j)].append(v+1)

    return empty_spots
```

Second, we intialize the state and find empty spots.

```
# initialize state
row_state = [0]*9
col_state = [0]*9
grid_state = [0]*9

empty_spots = getEmptySpots(board, 9, 9, row_state, col_state, grid_state)
print(row_state, col_state, grid_state)
sorted_empty_spots = sorted(empty_spots.items(), key=lambda x: len(x[1]))
print(sorted_empty_spots)
```

    [84, 305, 416, 164, 141, 98, 162, 281, 448] [248, 292, 128, 137, 483, 276, 2, 224, 309] [436, 337, 32, 200, 166, 37, 32, 393, 466]
    [((4, 4), [5]), ((6, 5), [7]), ((6, 8), [4]), ((7, 7), [3]), ((0, 3), [2, 6]), ((2, 0), [1, 2]), ((2, 3), [2, 3]), ((2, 4), [3, 4]), ((2, 5), [2, 4]), ((4, 1), [2, 5]), ((5, 1), [1, 5]), ((5, 3), [5, 9]), ((5, 5), [1, 4]), ((6, 4), [3, 5]), ((7, 0), [2, 3]), ((7, 6), [3, 6]), ((8, 5), [2, 6]), ((0, 2), [1, 2, 4]), ((0, 8), [2, 4, 8]), ((1, 1), [2, 4, 7]), ((1, 2), [2, 4, 7]), ((1, 7), [2, 3, 4]), ((2, 8), [2, 4, 7]), ((3, 1), [1, 2, 5]), ((3, 3), [5, 7, 9]), ((3, 5), [1, 4, 7]), ((4, 6), [5, 7, 9]), ((4, 7), [2, 5, 9]), ((5, 7), [4, 5, 9]), ((6, 0), [1, 3, 9]), ((6, 3), [3, 5, 7]), ((7, 1), [2, 7, 8]), ((7, 2), [2, 3, 7]), ((8, 0), [1, 2, 3]), ((0, 5), [2, 4, 6, 8]), ((0, 6), [1, 4, 8, 9]), ((0, 7), [1, 2, 4, 9]), ((1, 6), [3, 4, 7, 8]), ((1, 8), [2, 4, 7, 8]), ((3, 2), [1, 2, 5, 9]), ((3, 6), [4, 5, 7, 9]), ((3, 7), [2, 4, 5, 9]), ((4, 2), [2, 5, 6, 9]), ((5, 2), [1, 3, 5, 9]), ((5, 6), [4, 5, 8, 9]), ((8, 1), [1, 2, 4, 5]), ((8, 3), [2, 3, 5, 6]), ((8, 6), [1, 3, 4, 6]), ((2, 6), [1, 3, 4, 5, 7]), ((8, 2), [1, 2, 3, 4, 5]), ((6, 2), [1, 3, 4, 5, 7, 9])]

Traverse the empty_spots, and fill in.

```
def dfs_backtrack(empty_spots, index):
  if index == len(empty_spots):
    return True
  (i, j), vl = empty_spots[index]

  for v in vl: #try each value
    # check the state
    if checkState(i, j, v-1, row_state, col_state, grid_state):
      # set the state
      setState(i, j, v-1, row_state, col_state, grid_state)
      # mark the board
      board[i][j] = v
      if dfs_backtrack(empty_spots, index+1):
        return True
      else:
        #backtack to previouse state
        resetState(i, j, v-1, row_state, col_state, grid_state)
        #unmark the board
        board[i][j] = None
  return False
```

```
from graphviz import Digraph
dot = Digraph(comment='The Round Table', format='png')
dot.node_attr['shape']='ellipse'
dot.node(str((-1, -1)), label=str((-1, -1)))
count = 0
def dfs_backtrack(empty_spots, index, last_node):
  global count
  if index == len(empty_spots):
    return True
  (i, j), vl = empty_spots[index]
  ni, nj = -1, -1
  if index + 1 < len(empty_spots):
    (ni, nj), nvl = empty_spots[index + 1]

  for v in vl: #try each value
    # check the state
    if checkState(i, j, v-1, row_state, col_state, grid_state):

      cur_node = str((ni, nj, v))
      dot.node(str((ni, nj, v)), label=str((ni, nj))) # label shows, first is index
      dot.edge(last_node, str((ni, nj, v)), label=str(v))
      # set the state
      setState(i, j, v-1, row_state, col_state, grid_state)
      # mark the board
      board[i][j] = v
      if dfs_backtrack(empty_spots, index+1, cur_node):
        count -= 1
        return True
      else:
        #backtack to previouse state
        count -= 1
        resetState(i, j, v-1, row_state, col_state, grid_state)
        dot.edge(str((ni, nj, v)), last_node, label=str(v), _attributes={'color': 'red'})
        #unmark the board
        board[i][j] = None
  return False
```

```
ans = dfs_backtrack(sorted_empty_spots, 0, str((-1, -1)))
print(ans)
print(board)
# dot.render('test-output/sudoku_search_tree', view=True)
# dot
```

    True
    [[5, 3, 4, 6, 7, 8, 9, 1, 2], [6, 7, 2, 1, 9, 5, 3, 4, 8], [1, 9, 8, 3, 4, 2, 5, 6, 7], [8, 5, 9, 7, 6, 1, 4, 2, 3], [4, 2, 6, 8, 5, 3, 7, 9, 1], [7, 1, 3, 9, 2, 4, 8, 5, 6], [9, 6, 1, 5, 3, 7, 2, 8, 4], [2, 8, 7, 4, 1, 9, 6, 3, 5], [3, 4, 5, 2, 8, 6, 1, 7, 9]]

#### Arbitray variables ordering VS minimal domain first ordering

```
from copy import deepcopy
import time
class SudokoSolver():
  def __init__(self, board):
    self.original_board = deepcopy(board)
    self.board = deepcopy(board)
    self.n = len(board)
    assert (self.n == len(board[0]))
    # initialize state
    self.row_state = [0]*self.n
    self.col_state = [0]*self.n
    self.grid_state = [0]*self.n

  def _setState(self, i, j, v):
    self.row_state[i] |= 1 << v
    self.col_state[j] |= 1 << v
    grid_index = (i//3)*3 + (j//3)
    self.grid_state[grid_index] |= 1 << v

  def _resetState(self, i, j, v):
    self.row_state[i] &= ~(1 << v)
    self.col_state[j] &= ~(1 << v)
    grid_index = (i//3)*3 + (j//3)
    self.grid_state[grid_index] &= ~(1 << v)

  def _checkState(self, i, j, v):
    row_bit = (1 << v) & self.row_state[i] != 0
    col_bit = (1 << v) & self.col_state[j]  != 0
    grid_index = (i//3)*3 + (j//3)
    grid_bit = (1 << v) & self.grid_state[grid_index]  != 0
    return not row_bit and not col_bit and not grid_bit

  def reset(self):
    # initialize state
    self.row_state = [0]*self.n
    self.col_state = [0]*self.n
    self.grid_state = [0]*self.n
    self.board = deepcopy(self.original_board)

  def _getEmptySpots(self):
    ''' get empty spots and find its corresponding values in O(n*n)'''
    empty_spots = {}
    # initialize the state, and get empty spots
    for i in range(self.n):
      for j in range(self.n):
        if self.board[i][j]:
            # set that bit to 1
            self._setState(i, j, self.board[i][j]-1)
        else:
            empty_spots[(i,j)] = []

    # get possible values for each spot
    for i, j in empty_spots.keys():
      for v in range(self.n):
        if self._checkState(i, j, v):
          empty_spots[(i, j)].append(v+1)

    return empty_spots

  def helper(self, empty_spots, index):
    if index == len(empty_spots):
      return True
    (i, j), vl = empty_spots[index]

    for v in vl: #try each value
      # check the state
      if self._checkState(i, j, v-1):
        # set the state
        self._setState(i, j, v-1)
        # mark the board
        self.board[i][j] = v
        if self.helper(empty_spots, index+1):
          return True
        else:
          #backtack to previouse state
          self._resetState(i, j, v-1)
          #unmark the board
          self.board[i][j] = None
    return False

  def backtrackSolver(self):
    self.reset()
    empty_spots = self._getEmptySpots()
    empty_spots = [(k, v) for k, v in empty_spots.items() ]
    t0 = time.time()
    ans = self.helper(empty_spots, 0)
    print('total time: ', time.time() - t0)
    return ans

  def backtrackSolverSorted(self):
    self.reset()
    empty_spots = self._getEmptySpots()
    empty_spots = sorted(empty_spots.items(), key=lambda x: len(x[1]))
    t0 = time.time()
    ans = self.helper(empty_spots, 0)
    print('sorted total time: ', time.time() - t0)
    return ans
```

```
board = [[5, 3, None, None, 7, None, None, None, None],
         [6, None, None, 1, 9, 5, None, None, None],
         [None, 9, 8, None, None, None, None, 6, None],
         [8, None, None, None, 6, None, None, None, 3],
         [4, None, None, 8, None, 3, None, None, 1],
         [7, None, None, None, 2, None, None, None, 6],
         [None, 6, None, None, None, None, 2, 8, None],
         [None, None, None, 4, 1, 9, None, None, 5],
         [None, None, None, None, 8, None, None, 7, 9]]
solver = SudokoSolver(board)
solver.backtrackSolver()
solver.backtrackSolverSorted()
```

    total time:  0.02195119857788086
    sorted total time:  0.00042724609375

    True

#### Implementation

```
from copy import deepcopy
class Sudoku():
  def __init__(self, board):
    self.org_board = deepcopy(board)
    self.board = deepcopy(board)

  def init(self):
    self.A = set([i for i in range(1,10)])
    self.row_state = [set() for i in range(9)]
    self.col_state = [set() for i in range(9)]
    self.block_state = [[set() for i in range(3)] for i in range(3)]
    self.unfilled = []

    for i in range(9):
      for j in range(9):
          c = self.org_board[i][j]
          if c == 0:
              self.unfilled.append((i, j))
          else:
              self.row_state[i].add(c)
              self.col_state[j].add(c)
              self.block_state[i//3][j//3].add(c)

  def set_state(self, i, j, c):
    self.board[i][j] = c
    self.row_state[i].add(c)
    self.col_state[j].add(c)
    self.block_state[i//3][j//3].add(c)

  def reset_state(self, i, j, c):
    self.board[i][j] = 0
    self.row_state[i].remove(c)
    self.col_state[j].remove(c)
    self.block_state[i//3][j//3].remove(c)

  def _ret_len(self, args):
    i, j = args
    option = self.A - (self.row_state[i] | self.col_state[j] | self.block_state[i//3 ][j//3])
    return len(option)

  def solve(self):
    '''implement solver restricted spot selection and look ahead'''
    if len(self.unfilled) == 0:
      return True
    i, j = min(self.unfilled, key = self._ret_len)
    option = self.A - (self.row_state[i] | self.col_state[j] | self.block_state[i//3 ][j//3])
    #print(option)
    if len(option) == 0:
      return False
    self.unfilled.remove((i, j))
    for c in option:
      self.set_state(i, j, c)
      if self.solve():
        return True
      else:
        self.reset_state(i, j, c)
    # no candidate is valid, backtrack
    self.unfilled.append((i, j))
    return False

  def naive_solve(self):
    '''implement naitve solver without restricted spot selection or look ahead'''
    if len(self.unfilled) == 0:
      return True
    i, j = self.unfilled.pop()
    option = self.A - (self.row_state[i] | self.col_state[j] | self.block_state[i//3 ][j//3])
    for c in option:
      self.set_state(i, j, c)
      if self.naive_solve():
        return True
      else:
        self.reset_state(i, j, c)
    # no candidate is valid, backtrack
    self.unfilled.append((i, j))
    return False

```

```
board_easy = [[5, 3, 0, 0, 7, 0, 0, 0, 0],
         [6, 0, 0, 1, 9, 5, 0, 0, 0],
         [0, 9, 8, 0, 0, 0, 0, 6, 0],
         [8, 0, 0, 0, 6, 0, 0, 0, 3],
         [4, 0, 0, 8, 0, 3, 0, 0, 1],
         [7, 0, 0, 0, 2, 0, 0, 0, 6],
         [0, 6, 0, 0, 0, 0, 2, 8, 0],
         [0, 0, 0, 4, 1, 9, 0, 0, 5],
         [0, 0, 0, 0, 8, 0, 0, 7, 9]]

board_hard = [[3, 8, 0, 0, 0, 4, 0, 0, 0],
         [0, 0, 5, 0, 0, 0, 0, 1, 0],
         [0, 0, 1, 5, 0, 0, 0, 7, 0],
         [2, 0, 7, 0, 0, 5, 0, 0, 4],
         [0, 0, 0, 6, 7, 9, 0, 0, 0],
         [8, 0, 0, 1, 0, 0, 7, 0, 6],
         [0, 5, 0, 0, 0, 8, 2, 0, 0],
         [0, 4, 0, 0, 0, 0, 5, 0, 0],
         [0, 0, 0, 2, 0, 0, 0, 4, 1]]

# board_evil = [[0, 0, 0, 7, 0, 0, 0, 0, 4],
#               [8, 0, 0, 0, 0, 6, 0, 0, 0],
#               [6, 0, 0, 0, 9, 8, 7, 0, 0],
#               [0, 4, 0, 0, 6, 0, 9, 0, 7],
#               [0, 9, 0, 0, 0, 0, 0, 1, 0],
#               [1, 0, 8, 0, 2, 0, 0, 3, 0],
#               [0, 0, 3, 5, 4, 0, 0, 0, 8],
#               [0, 0, 0, 1, 0, 0, 0, 0, 5],
#               [2, 0, 0, 0, 0, 9, 0, 0, 0]]
```

```
for board in [board_easy, board_hard]:
  solver = Sudoku(board)

  import time
  t0 = time.time()
  solver.init()
  solver.naive_solve()
  print(solver.board)
  print('total time using naive solver: ', time.time()-t0, 's')

  t0 = time.time()
  solver.init()
  solver.solve()
  print(solver.board)
  print('total time using smart solver: ', time.time()-t0, 's')
```

    [[5, 3, 4, 6, 7, 8, 9, 1, 2], [6, 7, 2, 1, 9, 5, 3, 4, 8], [1, 9, 8, 3, 4, 2, 5, 6, 7], [8, 5, 9, 7, 6, 1, 4, 2, 3], [4, 2, 6, 8, 5, 3, 7, 9, 1], [7, 1, 3, 9, 2, 4, 8, 5, 6], [9, 6, 1, 5, 3, 7, 2, 8, 4], [2, 8, 7, 4, 1, 9, 6, 3, 5], [3, 4, 5, 2, 8, 6, 1, 7, 9]]
    total time using naive solver:  0.005067586898803711 s
    [[5, 3, 4, 6, 7, 8, 9, 1, 2], [6, 7, 2, 1, 9, 5, 3, 4, 8], [1, 9, 8, 3, 4, 2, 5, 6, 7], [8, 5, 9, 7, 6, 1, 4, 2, 3], [4, 2, 6, 8, 5, 3, 7, 9, 1], [7, 1, 3, 9, 2, 4, 8, 5, 6], [9, 6, 1, 5, 3, 7, 2, 8, 4], [2, 8, 7, 4, 1, 9, 6, 3, 5], [3, 4, 5, 2, 8, 6, 1, 7, 9]]
    total time using smart solver:  0.002804994583129883 s
    [[3, 8, 6, 7, 1, 4, 9, 2, 5], [4, 7, 5, 9, 2, 3, 6, 1, 8], [9, 2, 1, 5, 8, 6, 4, 7, 3], [2, 6, 7, 8, 3, 5, 1, 9, 4], [5, 1, 4, 6, 7, 9, 8, 3, 2], [8, 3, 9, 1, 4, 2, 7, 5, 6], [1, 5, 3, 4, 9, 8, 2, 6, 7], [7, 4, 2, 3, 6, 1, 5, 8, 9], [6, 9, 8, 2, 5, 7, 3, 4, 1]]
    total time using naive solver:  0.029914140701293945 s
    [[3, 8, 6, 7, 1, 4, 9, 2, 5], [4, 7, 5, 9, 2, 3, 6, 1, 8], [9, 2, 1, 5, 8, 6, 4, 7, 3], [2, 6, 7, 8, 3, 5, 1, 9, 4], [5, 1, 4, 6, 7, 9, 8, 3, 2], [8, 3, 9, 1, 4, 2, 7, 5, 6], [1, 5, 3, 4, 9, 8, 2, 6, 7], [7, 4, 2, 3, 6, 1, 5, 8, 9], [6, 9, 8, 2, 5, 7, 3, 4, 1]]
    total time using smart solver:  0.0038022994995117188 s

## Combinatorial Optimization Problems
* [Resources](https://www.coursera.org/learn/discrete-optimization/lecture/n2TGL/knapsack-1-intuition)

### Travelling Salesman Problem

Resources:

* [notes](https://www.mathematics.pitt.edu/sites/default/files/TSP.pdf)

```
from graphviz import Graph
dot = Graph(comment='The Round Table', format='png')
dot.node_attr['shape']='ellipse'
# nodes = ['1', '2', '3', '4']
# for n in nodes:
#   dot.node(n)
dot.edge('1', '2', label=str(10))
dot.edge('1', '3', label=str(15))
dot.edge('1', '4', label=str(20))
dot.edge('2', '4', label=str(25))
dot.edge('3', '4', label=str(30))
dot.edge('2', '3', label=str(35))

rank1 = [1, 4]
rank2 = [2, 3]
rank3 = [2, 3]
for rank in [rank1, rank2]:
  with dot.subgraph() as s:
    s.attr(rank='same')
    for node in rank:
      s.node(str(node))
dot.render('test-output/tsp_graph', view=True)
dot
```

```
# Import our modules that we are using
import matplotlib.pyplot as plt
import numpy as np
import math

# Create the vectors X and Y
x = np.array(range(10))
y = (x)**(x+1)
y2= [math.factorial(i) for i in x]
print(y)
print(y2)

# Create the plot
plt.plot(x,y, label='y = (x-1)**x')
plt.plot(x, y2, label='y = x!')

# Add a title
plt.title('My first Plot with Python')

# Add X and y Label
plt.xlabel('x axis')
plt.ylabel('y axis')

# Add a grid
plt.grid(alpha=.4,linestyle='-')

# Add a Legend
plt.legend()

# Show the plot
plt.show()
```

```
g = [[(1, 10), (2, 15), (3, 20)],
   [(0, 10), (2, 35),(3,25)],
   [(0, 15),(1,35),(3,30)],
   [(0,20),(1,25),(2,30)]]
```

```
# Implemented with graph search
def tsp(g, cur, path, mincost, cost, bused, ans):
  if len(path) == len(g): # we can only choose 0
    cost += g[cur][0][1]
    if cost < mincost[0]:
      mincost[0] = cost
      ans[0] = path[::]
    return
  for v, c in g[cur]:
    # constraint on permutation and cost
    if (not bused[v]) and (cost + c < mincost[0]):
        bused[v] = True
        path.append(v)
        cost += c
        tsp(g, v, path, mincost, cost, bused, ans)
        bused[v] = False
        path.pop()
        cost -= c
  return
```

```
import sys
cv = 0
path = [0]
mincost = [sys.maxsize]
cost = 0
bused = [False] * len(g)
bused[0] = True
ans = [[]]
tsp(g, cv, path, mincost, cost, bused, ans)
print(mincost[0], ans[0])
```

    80 [0, 1, 3, 2]

```
g = [{1: 10, 2: 15, 3:20},
   {0:10, 2:35,3:25},
   {0:15,1:35,3:30},
   {0:20,1:25,2:30}]
```

```
#Implement with permutation
def tsp(a, d, used, curr, ans, start, g, mincost, cost):
  if d == len(a):
    # Add the cost from last vertex to the start
    c = g[curr[-1]][start]
    cost += c
    if cost < mincost[0]:
      mincost[0] = cost
      ans[0] = curr[::] + [start]
    return

  for i in a:
    if not used[i] and cost + g[curr[-1]][i] < mincost[0] :
      cost += g[curr[-1]][i]
      curr.append(i)
      used[i] = True
      tsp(a, d + 1, used, curr, ans, start, g, mincost, cost)
      curr.pop()
      cost -= g[curr[-1]][i]
      used[i] = False
  return
```

```
import sys
mincost = [sys.maxsize]
bused = [False] * len(g)
bused[0] = True
start = 0
a = [i for i in range(1, len(g))]
ans = [[]]
tsp(a, 0, bused, [0], ans, start, g, mincost, 0)
print(mincost[0], ans[0])
```

    80 [0, 1, 3, 2, 0]

### Knapsack problem
Resources:
* [Branch and Bound](https://en.wikipedia.org/wiki/Branch_and_bound)
* [MILP](https://www.gurobi.com/resource/mip-basics/)
* [COPs](https://www.math.unipd.it/~luigi/courses/metmodoc1718/m02.meta.en.partial01.pdf)
#### **Knapsack**

#### Depth first branch and bound
DFS(backtracking) with branch and bound by estimating the total cost and compare it with **the** best found so far.

```
c = 10
w = [5, 8, 3]
v = [45, 48, 35]
```

```
import heapq

class BranchandBound:
  def __init__(self, c, v, w):
    self.best = 0
    self.c = c
    self.n = len(v)
    self.items = [(vi/wi, wi, vi) for _, (vi, wi) in enumerate(zip(v, w))]
    self.items.sort(key=lambda x: x[0], reverse=True)
    print(self.items)

  def estimate(self, idx, curval, left_cap):
    est = curval
    # use the v/w to estimate
    for i in range(idx, self.n):
      ratio, wi, _ = self.items[i]
      if left_cap - wi >= 0: # use all
        est += ratio * wi
        left_cap -= wi
      else: # use part
        est += ratio * (left_cap)
        left_cap = 0
    return est

  def dfs(self, idx, est, val, left_cap, status):
      if idx == self.n:
        self.best = max(self.best, val)
        return
      print(status, val, left_cap, est )

      _, wi, vi = self.items[idx]
      # Case 1: choose the item
      if left_cap - wi >= 0: # prune by constraint
        # Bound by estimate, increase value and volume
        if est > self.best:
          status.append(True)
          nest = self.estimate(idx+1, val+vi, left_cap - wi)
          self.dfs(idx+1, nest, val+vi, left_cap - wi, status)
          status.pop()

      # Case 2: not choose the item
      if est > self.best:
        status.append(False)
        nest =  self.estimate(idx+1, val, left_cap)
        self.dfs(idx+1, nest, val, left_cap, status)
        status.pop()
      return

  def bfs(self):
      # track val, cap, and idx is which item to add next
      q = [(-self.estimate(0, 0, self.c), 0, self.c, 0)] # estimate, val, left_cap, idx
      self.best = 0
      while q:
        est, val, left_cap, idx = heapq.heappop(q)
        est = -est
        _, wi, vi = self.items[idx]

        print(est, val, left_cap, idx, q, self.best, idx, vi)
        if idx == self.n - 1:
          self.best = max(self.best, val)
          continue

        # Case 1: choose the item
        nest = self.estimate(idx + 1, val + vi, left_cap - wi)
        if nest > self.best:
          heapq.heappush(q, (-nest, val + vi, left_cap - wi, idx + 1))

        # Case 2: not choose the item
        nest = self.estimate(idx + 1, val, left_cap)
        if nest > self.best:
          heapq.heappush(q, (-nest, val, left_cap, idx + 1))
      return

  def runDfs(self):
      self.dfs(0, self.estimate(0, 0, self.c), 0, self.c, [])
      return self.best

  def runBfs(self):
     self.bfs()
     return self.best
```

```
bnb = BranchandBound(c, v, w)
bnb.runDfs()
bnb.runBfs()
```

    [(11.666666666666666, 3, 35), (9.0, 5, 45), (6.0, 8, 48)]
    [] 0 10 92.0
    [True] 35 7 92.0
    [True, True] 80 2 92.0
    [True, False] 35 7 77.0
    [False] 0 10 75.0
    92.0 0 10 0 [] 0 0 35
    92.0 35 7 1 [(-75.0, 0, 10, 1)] 0 1 45
    92.0 80 2 2 [(-77.0, 35, 7, 2), (-75.0, 0, 10, 1)] 0 2 48
    77.0 35 7 2 [(-75.0, 0, 10, 1)] 80 2 48
    75.0 0 10 1 [] 80 1 45

    80

### Eight Queen

```
n=(64*63*62*61*60*59*58*57)/(8*7*6*5*4*3*2*1)
print(n)
```

    4426165368.0

```
class Solution:
    def solveNQueens(self, n):
        """
        :type n: int
        :rtype: List[List[str]]
        """
        # queen can move: vertically, horizontally, diagonally
        col_state = [False]*n
        #diag =[False]*n
        left_diag = [False]* (2*n-1) # x+y -> index
        right_diag = [False]* (2*n-1) # x+(n-1-y) ->index
        n_queen = [] # to track the positions
        ans = []
        board = [['.' for i in range(n)] for j in range(n)] #initialize as '.' we can try to flip
        def collect_solution():
            board = [['.' for i in range(n)] for j in range(n)]
            for i, j in enumerate(n_queen):
                board[i][j] = 'Q'

            for i in range(n):
                board[i] = ''.join(board[i])
            return board

        def is_valid(r, c):
            return not (col_state[c] or left_diag[r+c] or right_diag[r+(n-1-c)])

        def set_state(r, c, val):
            col_state[c] = val
            #diag[abs(r-c)] = val
            left_diag[r+c] = val
            right_diag[r+(n-1-c)] = val

        def backtrack(n_queen, k):
            if k == n: # a valid result
                ans.append(collect_solution())
                return
            # generate candidates for kth queen
            for col in range(n):
                if is_valid(k, col):
                    set_state(k, col, True)
                    n_queen.append(col)
                    backtrack(n_queen, k+1)
                    set_state(k, col, False)
                    n_queen.pop()

        backtrack(n_queen, 0)
        return ans

    def solveNQueens2(self, n):
      """
      :type n: int
      :rtype: List[List[str]]
      """
      n_queen = [] # to track the positions
      ans = []
      board = [['.' for i in range(n)] for j in range(n)] #initialize as '.' we can try to flip
      def collect_solution():
          board = [['.' for i in range(n)] for j in range(n)]
          for i, j in enumerate(n_queen):
              board[i][j] = 'Q'

          for i in range(n):
              board[i] = ''.join(board[i])
          return board

      def generate_candidate(n_queen, k, n):
        if k == 0: #the first row, then the candidates row is all columns
          return set([i for i in range(n)])
        # generate candidate in kth level based on previous levels
        candidates = set([i for i in range(n)])
        for r, c in enumerate(n_queen):
          if c in candidates:
            candidates.remove(c)
          c1 = c-(k-r)
          if c1 >=0 and c1 in candidates:
            candidates.remove(c1)
          c2 = c+(k-r)
          if c2 < n and c2 in candidates:
            candidates.remove(c2)
        return candidates

      def backtrack(n_queen, k):
          if k == n: # a valid result
              ans.append(collect_solution())
              return
          # generate candidates for kth queen
          candidates = generate_candidate(n_queen, k, n)
          for c in candidates:
              n_queen.append(c)
              backtrack(n_queen, k+1)
              n_queen.pop()

      backtrack(n_queen, 0)
      return ans
```

```
import time
s = Solution()
n = 4
t0 = time.time()
ans = s.solveNQueens(n)
print(ans)
t1 = time.time()
print('time: ', t1-t0)
ans2 = s.solveNQueens2(n)
t2 = time.time()
print('time: ', t2-t1)
```

    [['.Q..', '...Q', 'Q...', '..Q.'], ['..Q.', 'Q...', '...Q', '.Q..']]
    time:  0.0006678104400634766
    time:  0.0014612674713134766

#### Utilize symmetry

```
    def solveNQueensSymmetry(n):
      """
      :type n: int
      :rtype: List[List[str]]
      """
      n_queen = [] # to track the positions

      def generate_candidate(n_queen, s, k, n):
        if k == s: #apply symmetry
          candidates = set([i for i in range(n//2)])
        else:
          candidates = set([i for i in range(n)])

        for r, c in enumerate(n_queen):
          if c in candidates:
            candidates.remove(c)
          c1 = c-(k-r)
          if c1 >=0 and c1 in candidates:
            candidates.remove(c1)
          c2 = c+(k-r)
          if c2 < n and c2 in candidates:
            candidates.remove(c2)
        return candidates

      def backtrack(n_queen, s, k, ans):
          '''add s to track the start depth'''
          if k == n: # a valid result
              ans += 1
              return ans
          # generate candidates for kth queen
          candidates = generate_candidate(n_queen, s, k, n)
          for c in candidates:
              n_queen.append(c)
              ans = backtrack(n_queen, s, k+1, ans)
              n_queen.pop()
          return ans

      # deal with the left half of the first row
      ans = 0

      ans += backtrack(n_queen, 0, 0, 0)*2

      # deal with the left half of the second row
      if n%2 == 1:
        n_queen = [n//2]
        ans += backtrack(n_queen, 1, 1, 0)*2
      return ans
```

```
print(solveNQueensSymmetry(7))
```

    40

## Answers to Exercises


# Gregory-Leibniz Series
The Gregory series is a pi formula found by Gregory and Leibniz and obtained by plugging  into the Leibniz series,
http://mathworld.wolfram.com/GregorySeries.html
http://mathworld.wolfram.com/Pi.html
http://mathworld.wolfram.com/PiFormulas.html
Pi Day: 03/14 of each year

```python
from sympy import symbols, Eq, S, Sum, oo, init_printing
n  = symbols('n', integer=True)
init_printing()
```

```python
Eq(S.Pi, 4 * Sum(( ( (-1)**(n+1) ) / (2*n-1)), (n,1,oo)))
```

```python
from timeit import default_timer as timer
import numpy as np
```

## Function to calculate Pi

### Serial Processering

```python
def CalculatePi_Serial(N): # Do not recomend to use with larget N values
    start = timer()
    pi = 0
    for n in range(1, N):
        pi += (4.0 * ( (-1)**(n+1) ) / (2*n-1) )
    dt = timer() - start
    print('Serial calculate time   {:e} s'.format(dt))
    return pi
```

### Parallel Processering
#### Using Numba (https://numba.pydata.org/)

```python
from numba import autojit
@autojit
def CalculatePi_Parallel(N):
    start = timer()
    pi = 0
    for n in range(1, N):
        pi += (4.0 * ( (-1)**(n+1) ) / (2*n-1) )
    dt = timer() - start
    print('Parallel calculate time {:e} s'.format(dt))
    return pi
```

## Sample Pi Claculation (Serial)

```python
N =10000
PI = CalculatePi_Serial(N)
difference = PI-np.pi
print('  N = {}'.format(N))
print('* Gregory-Leibniz Series = {}'.format(PI))
print('* Pi value in Numpy      = {}'.format(np.pi))
print('* Difference             = {:e}'.format(difference))
print('* % Difference           = {:%}'.format(difference/np.pi))
```

    Serial calculate time   5.346737e-03 s
      N = 10000
    * Gregory-Leibniz Series = 3.1416926635905345
    * Pi value in Numpy      = 3.141592653589793
    * Difference             = 1.000100e-04
    * % Difference           = 0.003183%

## Run time performance Parallel vs. Serial

```python
N =1000000
PI_S = CalculatePi_Serial(N)
PI_P = CalculatePi_Parallel(N)
```

    Serial calculate time   6.469327e-01 s
    Parallel calculate time 1.385321e-01 s

## Comparing Accuract vs. N

```python
N = np.array([1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000])
PI = []
for n in N:
    PI.append(CalculatePi_Parallel(n))
PI = np.array(PI)
NumpyPI = np.pi
Difference = (PI-NumpyPI)/NumpyPI
```

    Parallel calculate time 9.873287e-06 s
    Parallel calculate time 9.979072e-05 s
    Parallel calculate time 1.409059e-03 s
    Parallel calculate time 1.146712e-02 s
    Parallel calculate time 1.213155e-01 s
    Parallel calculate time 9.106245e-01 s
    Parallel calculate time 8.647477e+00 s

```python
Difference = abs(PI-NumpyPI)/NumpyPI
for i in range(len(N)):
    print('* Pi [N = {:18}] = {:.36} ({:.8%})'.format(N[i], PI[i], Difference[i]))
print('* Pi [Numpy]                  = {:.36}'.format(np.pi))
```

    * Pi [N =               1000] = 3.14259365434004411810064993915148079 (0.03186284%)
    * Pi [N =              10000] = 3.14169266359053445114568603457883 (0.00318342%)
    * Pi [N =             100000] = 3.14160265368972035915362539526540786 (0.00031831%)
    * Pi [N =            1000000] = 3.14159365359077424884048923559021205 (0.00003183%)
    * Pi [N =           10000000] = 3.14159275358980139003506337758153677 (0.00000318%)
    * Pi [N =          100000000] = 3.14159266358932587337449149345047772 (0.00000032%)
    * Pi [N =         1000000000] = 3.14159265458805059267888282192870975 (0.00000003%)
    * Pi [Numpy]                  = 3.14159265358979311599796346854418516

```python
import matplotlib.pyplot as plt
plt.plot(N, PI, "r-o")
plt.axhline(np.pi)
plt.xscale('log')
plt.xlabel('N')
plt.ylabel('Pi')
plt.show()
```

    <Figure size 640x480 with 1 Axes>

```python
plt.plot(N[3:], PI[3:], "r-o")
plt.axhline(np.pi)
plt.xscale('log')
plt.xlabel('N')
plt.ylabel('Pi')
plt.show()
```

```python
plt.plot(N[3:], Difference[3:], "g-o")
plt.axhline(0)
plt.xscale('log')
plt.yscale('log')
plt.xlabel('N')
plt.ylabel('DIfference')
plt.show()
```


# Introduction
Support vector machines (SVMs) are a set of supervised learning methods used for classification, regression and outliers detection.

The advantages of support vector machines are:

* Effective in high dimensional spaces.
* Still effective in cases where number of dimensions is greater than the number of samples.
* Uses a subset of training points in the decision function (called support vectors), so it is also memory efficient.
* Versatile: different Kernel functions can be specified for the decision function. Common kernels are provided, but it is also possible to specify custom kernels.

The disadvantages of support vector machines include:

* If the number of features is much greater than the number of samples, avoid over-fitting in choosing Kernel functions and regularization term is crucial.
* SVMs do not directly provide probability estimates, these are calculated using an expensive five-fold cross-validation (see Scores and probabilities, below).

The support vector machines in scikit-learn support both dense (numpy.ndarray and convertible to that by numpy.asarray) and sparse (any scipy.sparse) sample vectors as input. However, to use an SVM to make predictions for sparse data, it must have been fit on such data. For optimal performance, use C-ordered numpy.ndarray (dense) or scipy.sparse.csr_matrix (sparse) with dtype=float64.

### Definition scikit-learn
class sklearn.svm.SVC(C=1.0, kernel=’rbf’, degree=3, gamma=’auto_deprecated’, coef0=0.0, shrinking=True, probability=False, tol=0.001, cache_size=200, class_weight=None, verbose=False, max_iter=-1, decision_function_shape=’ovr’, random_state=None)

# Mathematical Formulation

```python
from IPython.display import Image
Image(filename="svm_plot_separating_hyperplane_0011.png")
```

A support vector machine constructs a hyper-plane or set of hyper-planes in a high or infinite dimensional space, which can be used for classification, regression or other tasks. Intuitively, a good separation is achieved by the hyper-plane that has the largest distance to the nearest training data points of any class (so-called functional margin), since in general the larger the margin the lower the generalization error of the classifier

### Classification
Given vectors $x_i \in \mathbb{R}^p$ in tow classes and a vector $y \in \{1, -1\}^n$ SVC solves the problem:

\begin{align}\begin{aligned}\min_ {w, b, \zeta} \frac{1}{2} w^T w + C \sum_{i=1}^{n} \zeta_i\\\begin{split}\textrm {subject to } & y_i (w^T \phi (x_i) + b) \geq 1 - \zeta_i,\\
& \zeta_i \geq 0, i=1, ..., n\end{split}\end{aligned}\end{align}

its dual is

\begin{align}\begin{aligned}\min_{\alpha} \frac{1}{2} \alpha^T Q \alpha - e^T \alpha\\\begin{split}
\textrm {subject to } & y^T \alpha = 0\\
& 0 \leq \alpha_i \leq C, i=1, ..., n\end{split}\end{aligned}\end{align}

where  is the vector of all ones, $C  \gt 0$ is the upper bound, Q is an n  by n positive semidefinite matrix, $Q_{ij} \equiv y_i y_j K(x_i, x_j)$, where $K(x_i, x_j) = \phi (x_i)^T \phi (x_j)$ is the kernel. Here training vectors are implicitly mapped into a higher (maybe infinite) dimensional space by the function $\phi$.

The decision function is:

$\operatorname{sgn}(\sum_{i=1}^n y_i \alpha_i K(x_i, x) + \rho)$

### Regression

Given training vectors $x_i \in \mathbb{R}^p$, i=1,…, n, and a vector $y \in \mathbb{R}^n$ $\epsilon$-SVR solves the following primal problem:

\begin{align}\begin{aligned}\min_ {w, b, \zeta, \zeta^*} \frac{1}{2} w^T w + C \sum_{i=1}^{n} (\zeta_i + \zeta_i^*)\\\begin{split}\textrm {subject to } & y_i - w^T \phi (x_i) - b \leq \varepsilon + \zeta_i,\\
                      & w^T \phi (x_i) + b - y_i \leq \varepsilon + \zeta_i^*,\\
                      & \zeta_i, \zeta_i^* \geq 0, i=1, ..., n\end{split}\end{aligned}\end{align}

Its dual is

\begin{align}\begin{aligned}\min_{\alpha, \alpha^*} \frac{1}{2} (\alpha - \alpha^*)^T Q (\alpha - \alpha^*) + \varepsilon e^T (\alpha + \alpha^*) - y^T (\alpha - \alpha^*)\\\begin{split}
\textrm {subject to } & e^T (\alpha - \alpha^*) = 0\\
& 0 \leq \alpha_i, \alpha_i^* \leq C, i=1, ..., n\end{split}\end{aligned}\end{align}

where $e$ is the vector of all ones, $C > 0$ is the upper bound, Q is an  by  positive semidefinite matrix, $Q_{ij} \equiv K(x_i, x_j) = \phi (x_i)^T \phi (x_j)$ is the kernel. Here training vectors are implicitly mapped into a higher (maybe infinite) dimensional space by the function $\phi$.

The decision function is:

$\sum_{i=1}^n (\alpha_i - \alpha_i^*) K(x_i, x) + \rho$

These parameters can be accessed through the members $dual_coef_$ which holds the difference , $support_vectors_$ which holds the support vectors, and $intercept_$ which holds the independent term $\rho$.

```python
Image(filename="precission-recall.png")
```

```python
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, MinMaxScaler, QuantileTransformer
from sklearn.metrics import classification_report, confusion_matrix
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm
np.random.seed(100)

```

```python
def choose_scaler(xtrain, xtest, name='Standard'):
    scaler = None
    if name == 'Standard':
        scaler = StandardScaler()
    elif name == 'MinMax':
        scaler = MinMaxScaler()
    else:
        scaler = QuantileTransformer()

    xtrain = scaler.fit_transform(xtrain)
    xtest = scaler.transform(xtest)
    return xtrain, xtest
```

```python

data = load_breast_cancer()
print(data['feature_names'])
print('')
print('.' * 50)
x = data['data']
y = data['target']

xtrain, xtest, ytrain, ytest = train_test_split(x, y, shuffle=True)
print('shape of xtrain:', xtrain.shape)
print('shape of ')

xtrain, xtest = choose_scaler(xtrain, xtest, name='MinMax')
print('the scaled values are:')
print(xtrain)

clf = svm.SVC(gamma='scale')
clf.fit(xtrain, ytrain)

# step 3 make predictitions with test set
y_pred_train = clf.predict(xtrain)
y_pred = clf.predict(xtest)

# step 4 use confusion matrix and classification report
print('-' * 50)
print('Training set:')
print('Classification report:')
print(classification_report(ytrain, y_pred_train))
print('')
print('Confusion matrix:')
print(confusion_matrix(ytrain, y_pred_train))
print('-' * 50)

print('.' * 50)
print('Test set:')
print('Classification report:')
print(classification_report(ytest, y_pred))
print('')
print('Confusion matrix:')
print(confusion_matrix(ytest, y_pred))
print('.' * 50)
```

    ['mean radius' 'mean texture' 'mean perimeter' 'mean area'
     'mean smoothness' 'mean compactness' 'mean concavity'
     'mean concave points' 'mean symmetry' 'mean fractal dimension'
     'radius error' 'texture error' 'perimeter error' 'area error'
     'smoothness error' 'compactness error' 'concavity error'
     'concave points error' 'symmetry error' 'fractal dimension error'
     'worst radius' 'worst texture' 'worst perimeter' 'worst area'
     'worst smoothness' 'worst compactness' 'worst concavity'
     'worst concave points' 'worst symmetry' 'worst fractal dimension']

    ..................................................
    shape of xtrain: (426, 30)
    shape of
    the scaled values are:
    [[0.39058824 0.66       0.40235294 ... 0.55529412 0.08705882 0.63294118]
     [0.55647059 0.48235294 0.56705882 ... 0.62352941 0.53176471 0.72470588]
     [0.10588235 0.28117647 0.10823529 ... 0.4        0.39882353 0.79529412]
     ...
     [0.24705882 0.32470588 0.22588235 ... 0.12235294 0.42352941 0.16      ]
     [0.42705882 0.40235294 0.44       ... 0.35176471 0.46823529 0.47176471]
     [0.04705882 0.08235294 0.04941176 ... 0.30823529 0.92235294 0.71764706]]
    --------------------------------------------------
    Training set:
    Classification report:
                  precision    recall  f1-score   support

               0       0.99      0.98      0.99       156
               1       0.99      1.00      0.99       270

        accuracy                           0.99       426
       macro avg       0.99      0.99      0.99       426
    weighted avg       0.99      0.99      0.99       426

    Confusion matrix:
    [[153   3]
     [  1 269]]
    --------------------------------------------------
    ..................................................
    Test set:
    Classification report:
                  precision    recall  f1-score   support

               0       0.98      0.95      0.96        56
               1       0.97      0.99      0.98        87

        accuracy                           0.97       143
       macro avg       0.97      0.97      0.97       143
    weighted avg       0.97      0.97      0.97       143

    Confusion matrix:
    [[53  3]
     [ 1 86]]
    ..................................................

    D:\anaconda\lib\site-packages\sklearn\preprocessing\data.py:2239: UserWarning: n_quantiles (1000) is greater than the total number of samples (426). n_quantiles is set to n_samples.
      % (self.n_quantiles, n_samples))

# Cross validation

When evaluating different settings (“hyperparameters”) for estimators, such as the C setting that must be manually set for an SVM, there is still a risk of overfitting on the test set because the parameters can be tweaked until the estimator performs optimally. This way, knowledge about the test set can “leak” into the model and evaluation metrics no longer report on generalization performance. To solve this problem, yet another part of the dataset can be held out as a so-called “validation set”: training proceeds on the training set, after which evaluation is done on the validation set, and when the experiment seems to be successful, final evaluation can be done on the test set.

However, by partitioning the available data into three sets, we drastically reduce the number of samples which can be used for learning the model, and the results can depend on a particular random choice for the pair of (train, validation) sets.

A solution to this problem is a procedure called cross-validation (CV for short). A test set should still be held out for final evaluation, but the validation set is no longer needed when doing CV. In the basic approach, called k-fold CV, the training set is split into k smaller sets (other approaches are described below, but generally follow the same principles). The following procedure is followed for each of the k “folds”:

* A model is trained using  of the folds as training data;
* The resulting model is validated on the remaining part of the data (i.e., it is used as a test set to compute a performance measure such as accuracy).

The performance measure reported by k-fold cross-validation is then the average of the values computed in the loop. This approach can be computationally expensive, but does not waste too much data (as is the case when fixing an arbitrary validation set), which is a major advantage in problems such as inverse inference where the number of samples is very small.

```python

```

```python
from IPython.display import Image
Image(filename="grid_search_cross_validation.png")
```

```python
# compute accuracy
clf.score(xtest, ytest)
```

    0.972027972027972

## Cross-validation with SVC

```python
from sklearn.model_selection import cross_val_score
```

```python
# note that all data can be used
clf = svm.SVC(gamma='scale')
scores = cross_val_score(clf, x, y, cv=5)
scores
```

    array([0.85217391, 0.89565217, 0.92920354, 0.94690265, 0.9380531 ])

```python
print("Accuracy: %0.2f (+/- %0.2f)" % (scores.mean(), scores.std() * 2))
```

    Accuracy: 0.91 (+/- 0.07)

notebooks/.ipynb_checkpoints/5. Support Vector Machines-checkpoint.ipynb


# Hybrid Monte Carlo

## Markov Models

In this notebook we analyse the evolution of HJM / Markov models. These models arise e.g. from Quasi Gaussian rates models or Markov Futures models.

We assume the model is driven by $d$-dimensional state variable $x$ and $d\times d$-dimensional auxilliary variable $y$.

The state variable $x$ follows a mean-reverting dynamic
$$
  dx(t) = \left[ \theta(y,t) - \chi(t) x(t) \right] dt + \sigma(t)^\top dW(t).
$$
Model parameters are $d\times d$-matrix of volatilities $\sigma(t)$ and diagonal $d\times d$-matrix of mean reversion speed parameters $\chi(t)$. The $d$-dimensional drift vector $\theta(y,t)$ couples state variables $x$ and auxilliary variables $y$.

The auxilliary variables $y$ follow the convection equation
$$
  dy(t) = \left[ \sigma(t)^\top \sigma(t) - \chi(t) y(t) - y(t) \chi(t) \right] dt.
$$

### Quasi-Gaussian Model

For Quasi-Gaussian rates models the drift term becomes
$$
  \theta(y,t) = y(t) \mathbb{1}.
$$

### Markov Futures Model

For Markov Futures models the drift term becomes
$$
  \theta(y,t) = \frac{1}{2} \left[ y(t)\chi(t) - \sigma(t)^\top \sigma(t) \right] \mathbb{1}.
$$

### Notation

We use the following functions to structure our notation:
$$
  H(s,t)   = \text{diag}\left\{ h_i(s,t)\right\},
  \quad \text{with} \quad
  h_i(s,t) = \exp\left\{ -\int_s^t \chi_i(u)du \right\}
$$
and
$$
  G(s,t)  = \int_s^t H(s,u)du = \int_s^t H(u,t)du.
$$
Functions $G$ and $H$ are related via $G'(s,t)=\partial G(s,t)/\partial t = H(s,t)$.

Moreover, we denote the instantanous variance as
$$
  V(t) = \sigma(t)^\top \sigma(t).
$$

## Evolution of State and Auxilliary Variables

In this section we derive a representation of the state and auxilliary variables. This representation is the basis for Monte-Carlo simulation.

We consider a time intervall from $s$ to $t$ ($s<t$). To simplify notation and derivation we assume model parameters are constant on that intervall. That is $\sigma(u)=\sigma$ and $\chi(u)=\chi$ for $u\in (s, t]$.

Note that even though model parameters are assumed (piece-wise) constant the drift term $\theta(u,t)$ is modelled exact. This is required because this quantity grows monotonically in time.

### Auxilliary Variables

The dynamics for $y$ are
$$
  dy(t) = \left[ V(t) - \chi(t) y(t) - y(t) \chi(t) \right] dt.
$$
The solution to this linear systems of ODEs is given by
$$
 y(t) = H(s,t)y(s)H(s,t) +
        \underbrace{\int_s^t H(u,t) V(u) H(u,t)du}_{M(s,t)}.
$$

For constant parameters the integral $M(s,t) = \left[ M_{i,j}(s,t) \right]$ is given as
$$
  M_{i,j}(s,t) = V_{i,j} \frac{1 - e^{-(\chi_i + \chi_j)(t-s)}}{\chi_i + \chi_j}.
$$
The elements of the first term $H(s,t)y(s)H(s,t)$ are $y_{i,j}(s)e^{-(\chi_i + \chi_j)(t-s)}$. This yields a representation of the elements $y_{i,j}(t)$ which we will use later on.

We get the representation of auxilliary variable elements as
\begin{align}
  y_{i,j}(t) &= y_{i,j}(s)e^{-(\chi_i + \chi_j)(t-s)} +
                V_{i,j} \frac{1 - e^{-(\chi_i + \chi_j)(t-s)}}{\chi_i + \chi_j} \\
             &= \left[ y_{i,j}(s) - \frac{V_{i,j}}{\chi_i + \chi_j} \right]
                 e^{-(\chi_i + \chi_j)(t-s)} +
                 \frac{V_{i,j}}{\chi_i + \chi_j}. \\
             &= a_{i,j} e^{-(\chi_i + \chi_j)(t-s)} + b_{i,j}
\end{align}
with $b_{i,j} = V_{i,j} / (\chi_i + \chi_j)$ and $a_{i,j} = V_{i,j} - b_{i,j}$.

Substituting $B = \left[ b_{i,j} \right]_{i,j}$ we get the matrix representation $A = y(s)-B$ and
$$
  y(t) = H(s,t) A H(s,t) + B.
$$

### State Variable

We have
$$
  dx(t) = \left[ \theta(y,t) - \chi(t) x(t) \right] dt + \sigma(t)^\top dW(t).
$$

The solution is given as
$$
 x(t) = H(s,t) \left[ x(s) + \int_s^t H(s,u)^{-1}
        \left[ \theta(y,u) du + \sigma(u)^\top dW(u) \right] \right].
$$

Covariance conditional on time $s$ becomes
$$
  \text{Cov}_s \left[x(t)\right] = \int_s^t H(u,t) \sigma(u)^\top \sigma(u) H(u,t) du.
$$
It turns out that
$$
  \text{Cov}_s \left[x(t)\right] = \int_s^t H(u,t) V(u) H(u,t) du = M(s,t).
$$

Conditional expectation is given as
$$
  \mathbb{E}_s\left[x(t)\right] = H(s,t) x(s) + \int_s^t H(u,t) \theta(y,u) du.
$$

For general drift functions $\theta(y,t)$ and with the representation $y(t) = H(s,t) A H(s,t) + B$ we can formulate $f(u) =  H(u,t) \theta\left(y(u),u\right)$. Then we can apply standard quadrature methods to calculate $\int_s^t f(u) du$. This typically works well since functions involved are smooth.

### Integrated Auxilliary Variable

The conditional expectation of the state variable requires calculation of $\int_s^t H(u,t) \theta(y,u) du$. In relevant applications the drift function $\theta(y,u)$ is an affine function of $y(u)$. In such situations we can calculate the integral.

As an intermediate step we calculate $\int_s^t H(u,t) y(u) du$. Using $y(u)=H(s,u) A H(s,u) + B$ yields
\begin{align}
  \int_s^t H(u,t) y(u) du
  &= \int_s^t H(u,t) \left[ H(s,u) A H(s,u) + B \right] du \\
  &= \int_s^t H(s,t) A H(s,u) du + \int_s^t H(u,t)B du \\
  &= H(s,t) A \int_s^t H(s,u) du + \int_s^t H(u,t) du B \\
  &= H(s,t) A G(s,t) + G(s,t) B.
\end{align}

We can also substitute back $A = y(s)-B$. This yields
\begin{align}
  \int_s^t H(u,t) y(u) du
  &= H(s,t) \left[ y(s) - B \right] G(s,t) + G(s,t) B \\
  &= H(s,t) y(s) G(s,t) - H(s,t) B G(s,t) + G(s,t) B \\
  &= H(s,t) y(s) G(s,t) + \left[G(s,t) B - H(s,t) B G(s,t) \right].
\end{align}

We have a closer look at the term $C = \left[G(s,t) B - H(s,t) B G(s,t) \right]$. The elements $C_{i,j}$ are
\begin{align*}
  C_{i,j} &= \frac{1-e^{-\chi_i(t-s)}}{\chi_i} \frac{V_{i,j}}{\chi_i + \chi_j} -
             e^{-\chi_i(t-s)} \frac{V_{i,j}}{\chi_i + \chi_j} \frac{1-e^{-\chi_j(t-s)}}{\chi_j} \\
          &= \frac{V_{i,j}}{\chi_i + \chi_j} \left[
               \frac{1-e^{-\chi_i(t-s)}}{\chi_i} - e^{-\chi_i(t-s)} \frac{1-e^{-\chi_j(t-s)}}{\chi_j}
               \right].
\end{align*}
For the diagonal elements $C_{i,i}$ we further get
\begin{align*}
  C_{i,i} &= \frac{V_{i,j}}{2 \chi_i} \left[
               \frac{1-e^{-\chi_i(t-s)}}{\chi_i} - e^{-\chi_i(t-s)} \frac{1-e^{-\chi_i(t-s)}}{\chi_i} \right] \\
          &= \frac{V_{i,j}}{2 \chi_i} \frac{1-e^{-\chi_i(t-s)}}{\chi_i} \left[
               1 - e^{-\chi_i(t-s)} \right] \\
          &= \frac{V_{i,j}}{2} \left[ \frac{1-e^{-\chi_i(t-s)}}{\chi_i} \right]^2.
\end{align*}

### Quasi Gaussian Model Drift and Expectation

The drift term in Quasi Gaussian rates models is just $\theta(y,t) = y(t) \mathbb{1}$. This yields the conditional expectation
$$
  \mathbb{E}_s\left[x(t)\right] = H(s,t) \left[ x(s) + A G(s,t) \mathbb{1} \right] + G(s,t) B  \mathbb{1}
$$
or equivalently
$$
  \mathbb{E}_s\left[x(t)\right] = H(s,t) \left[ x(s) + y(s) G(s,t) \mathbb{1} \right] + \left[G(s,t) B - H(s,t)  B G(s,t) \right] \mathbb{1}.
$$

### Futures Model Drift and Expectation

For Futures Model the drift is
$\theta(y,t) = \frac{1}{2} \left[ y(t)\chi(t) - \sigma(t)^\top \sigma(t) \right] \mathbb{1}$.
We can use linearity of the integral operator and get
\begin{align}
  \int_s^t H(u,t) \theta(y,u) du
  &= \int_s^t H(u,t) \frac{1}{2} \left[ y(t)\chi(t) - \sigma(t)^\top \sigma(t) \right] \mathbb{1} du \\
  &= \frac{1}{2} \int_s^t H(u,t) \left[ y(t)\chi - V \right] \mathbb{1} du \\
  &= \frac{1}{2} \left[ \int_s^t H(u,t) y(t) du  \chi\mathbb{1} -  \int_s^t H(u,t) du V  \mathbb{1} \right] \\
  &= \frac{1}{2} \left\{ \left[ H(s,t) A G(s,t) + G(s,t) B \right] \chi\mathbb{1} -  G(s,t) V  \mathbb{1} \right\}.
\end{align}

This gives the conditional expectation of the state variable as
$$
  \mathbb{E}_s\left[x(t)\right] = H(s,t) x(s) +
  \frac{1}{2} \left\{ \left[ H(s,t) A G(s,t) + G(s,t) B\right] \chi -  G(s,t) V \right\}\mathbb{1}.
$$

## Futures Reconstruction

Using the Markov Model representation the futures price $F(t,T)$ can be reconstructed from state variables $x(t)$ and auxilliary variables $y(t)$ via
$$
  F(t,T) = F(0,T) * e^{X(t,T)}
$$
with
$$
  X(t,T) = \mathbb{1}^\top H(t,T) \left[ x(t) +
      \frac{1}{2} y(t) \left(I - H(t,T)\right)\mathbb{1}
      \right].
$$

## Model Test

```python
import sys
sys.path.append('../')  # make python find our modules
import numpy as np

from hybmc.models.MarkovFutureModel import MarkovFutureModel
```

We set up a simple 2-factor model.

```python
d = 2
times = np.array([0.0])
sigmaT = np.zeros([1,2,2])
sigmaT[0,:,:] = np.array([[ 0.10, 0.15 ],
                          [ 0.20, 0.25 ]])
chi = np.array([0.10, 0.20])
#
model = MarkovFutureModel(None,d,times,sigmaT,chi) # future curve is not yet implemented
```

Using the model we simulate the state variables.

```python
from hybmc.simulations.McSimulation import McSimulation
times = np.linspace(0.0, 5.0, 2)
nPaths = 2**13
seed = 314159265359
sim = McSimulation(model,times,nPaths,seed,False,showProgress=True)
```

Now we can calculate futures prices and analyse the terminal distrivution.

```python
idx = -1
t = times[idx]
dT = 1.0
T = t + dT
F = np.array([ model.futurePrice(t,T,X,None) for X in sim.X[:,idx,:] ])
Fav = np.mean(F)
sigma = np.std(F) / np.sqrt(t)
print('t: %6.2f, T: %6.2f, F: %6.4f, sigma: %6.4f' % (t,T,Fav,sigma) )
```

## Andersen Parametrisation

We test another parametrisation taken from Andersen 2008, sec. 7.6.

```python
def sigmaT(sigma_0, sigma_infty, rho_infty):
    h1 = -sigma_infty + rho_infty * sigma_0
    h2 = sigma_0 * np.sqrt(1.0 - rho_infty**2)
    hi = sigma_infty
    return np.array([ [h1, h2], [hi, 0.0] ])

def chi(kappa):
    return np.array([ kappa, 0.0 ])
```

```python
# Andersen example
kappa       = 1.35
sigma_0     = 0.50
sigma_infty = 0.17
rho_infty   = 0.50

sigmaT_ = sigmaT(sigma_0,sigma_infty,rho_infty)
chi_    = chi(kappa)
print(sigmaT_)
print(chi_)
```

```python
d = 2
times = np.array([0.0])
model = MarkovFutureModel(None,d,times,np.array([ sigmaT_ ]),chi_)
```

```python
times = np.linspace(0.0, 5.0, 2)
nPaths = 2**13
seed = 314159265359
sim = McSimulation(model,times,nPaths,seed,False,showProgress=True)
```

```python
idx = -1
t = times[idx]
dT = 1.0
T = t + dT
F = np.array([ model.futurePrice(t,T,X,None) for X in sim.X[:,idx,:] ])
Fav = np.mean(F)
sigma = np.std(F) / np.sqrt(t)
print('t: %6.2f, T: %6.2f, F: %6.4f, sigma: %6.4f' % (t,T,Fav,sigma) )
```

```python

```