算法强化班小结

Chapter 1 Cracking the Follow Up Interview Questions

同向双指针模板O(n)

j = 0
for i in range(n):
    //挪动 i 的时候,看看 j 最多挪动到哪儿。
    //j 代表的是 subarray 右断点的 index + 1 的位置。因此 j - i 是 Subarray 的长度
    while j < n and 当前 subarray 不满足条件:
        j += 1
        拓宽当前subarray
    
    if 当前 subarray 满足条件:
        打擂台,看看是不是最优得

    将 nums[i] 移出当前 subarray

要点:

外层循环依然从0到n-1遍历每一个位置

内层循环“不回头”,一直++到最优的位置. 内层最多n次

  1. Minimum Size Subarray Sum
  2. Longest Substring Without Repeating Characters
  3. Minimum Window Substring
  4. Longest Substring with At Most K(Two) Distinct Characters

快慢指针类

  1. Remove Nth Node From End of List
  2. Find the Middle of Linked List
  3. Linked List Cycle I, II

 

Find Kth的Follow Up问题

求第K小元素及其Follow Up

单个数组,多个数组,矩阵

  1. Kth smallest numbers in unsorted array
    1. quick select O(n) 离线算法
    2. maxheap O(nlogk) 在线算法
  2. Kth Largest in N Arrays
    1. 全部做quick select O(NM) 未排序时最佳
    2. 每个数组分别排序O(N MlogM) + maxheap 多路归并 O(klogN) + 建堆 O(N)
    3. 如果有序可以二分答案 O(NlogM logRange)
  3. Kth smallest element in a sorted matrix
    1. 看作K个排序数组
    2. k比较小时 用minheap,pop一个数最可能的次小数是他右边和下面的数 (最多候选数是min(k,m,n)),用set记录visited, O(klogn)
    3. k 大时可以二分答案
  4. Kth smallest sum in two sorted arrays
    1. 转化为矩阵,更形象,和上题一样
    2. 两两和成为一个矩阵,和上面那题一样的sorted matrix

 

Follow Up的出题规律

数据所在数据结构的变化

Unsorted Array / Sorted Array

K Unsorted Arrays / k Sorted Arrays

Two Sorted Arrays / Sorted Matrix

Binary Search Tree / Linked List

 

Chapter 2 Data Structure - Union Find & Trie

并查集Union Find

一种用于支持集合快速合并和查找操作的数据结构

O(1)    合并两个集合- Union

O(1)    查询元素所属集合- Find

Union find 是一棵多叉树

如何实现Union Find

底层数据结构

  • 父亲表示法,用一个数组/哈希表记录每个节点的父亲是谁。
  • father[“Nokia”] = “Microsoft”
  • father[“Instagram”] = “Facebook”

查询所在集合

  • 用所在集合最顶层的老大哥节点来代表这个集合

合并两个集合

  • 找到两个集合中最顶层的两个老大哥节点A和B
  • father[A] = B // or father[B] = A如果无所谓谁合并谁的话

代码模板

初始化

使用哈希表或者数组来存储每个节点的父亲节点

如果节点不是连续整数的话,就最好用哈希表来存储

最开始所有的父亲节点都指向自己

 

路径压缩

沿着父亲节点一路往上走就能找到老大哥

在找到老大哥以后,还需要把一路上经过的点都指向老大哥

分为两种实现方法:递归vs非递归

推荐非递归

 

集合合并

找到两个元素所在集合的两个老大哥A和B

将其中一个老大哥的父指针指向另外一个老大哥

class UnionFind{
private:
    unordered_map <int, int> father;
public:
    UnionFind(int n){
        for(int i = 1; i < n+1; i++){
            father[i] = i;
        }
    }
    void union(int a, int b){
        father[find(a)] = find(b);
    }
    
    // find the father and assign to the common father
    int find(int node){
        vector<int> path;
        while(node != father[node]){
            path.push_back(node);
            node = father[node];
        }
        
        for(auto n : path){
            father[n] = node;
        }
        return node;
    }
    
};
  1. Connecting Graph
    1. 图的连通性
    2. 完全用union find 模板
  2. Connecting Graph II
    1. 获得某个集合的元素个数
    2. 根节点记录元素个数
  3. Connecting Graph III
    1. 获得集合个数
    2. 一开始是N,每connect一次减一

并查集可以做的事情总结

  1. 合并两个集合
  2. 查询某个元素所在集合
  3. 判断两个元素是否在同一个集合
  4. 获得某个集合的元素个数
  5. 统计当前集合个数

 

  1. Number of Islands II 
    1. 离线算法(BFS) vs在线算法(UnionFind)
  2. Graph Valid Tree
    1. Tree => n个点,n-1条边 + 一个联通块
    2. BFS和union find都可以
  3. Accounts Merge
    1. 这题点是人,边是两个邮箱被同一个人用的时候
    2. forward index 人=>邮箱
    3. inverted index 邮箱=>人 相同email的人是连通的(同一个人)

跟连通性有关的问题都可以使用BFS和Union Find

什么时候无法使用Union Find?

需要拆开两个集合的时候无法使用Union Find

其他Union Find的练习题

  1. Set Union
    1. 类似accounts merge
  2. Surrounded regions
    1. BFS or union find
  3. Maximum association set
    1. 找到最大的关联集合,在集合合并的时候打擂台即可

 

字典树 Trie (Prefix Tree)

Trie树的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

假设字符的种数有m个,有若干个长度为n的字符串构成了一个Trie树,则每个节点的出度为m(即每个节点的可能子节点数量为m),Trie树的高度为n。很明显我们浪费了大量的空间来存储字符,此时Trie树的最坏空间复杂度为O(m^n)。也正由于每个节点的出度为m,所以我们能够沿着树的一个个分支高效的向下逐个字符的查询,而不是遍历所有的字符串来查询,此时Trie树的最坏时间复杂度为O(n)
 

Trie的考点

  • 实现一个Trie
  • 比较Trie和Hash的优劣
    • Trie <=> hash
  • 字符矩阵类问题使用Trie比Hash更高效

Hash vs Trie

  • 互相可替代
  • Trie耗费更少的空间
  • 单次查询Trie耗费更多的时间(复杂度相同 O(L),Trie系数大一些)
    • Trie 的node不是在内存连续存储,要进行多次内存寻址

 Trie 代码模板

class TrieNode{
public:
    vector<TrieNode*> children;
    bool is_word;
    TrieNode(){
        is_word = false;
        children = vector<TrieNode*>(26,NULL);
    }
};

class Trie {
private:
    TrieNode* root;
public:
    Trie() {
        // initialize the root node
        root = new TrieNode();
    }
    
    void insert(string &word) {
        TrieNode *node = root;
        for(char c : word){
            //check if the character is already in the trie
            if(node->children[c-'a'] == NULL){
                node->children[c-'a'] = new TrieNode();
            }
            node = node->children[c-'a'];
        }
        node->is_word = true;
    }
    
    //if the word/prefix is in the trie.
    bool find(string &word) {
        TrieNode *node = root;
        for(char c : word){
            //check if the character is already in the trie
            if(node->children[c-'a'] == NULL){
                return NULL;
            }
            node = node->children[c-'a'];
        }
        return node;
    }
  1. Implement Trie
  2. Add and Search Word
    1. "." 只要后面字符能匹配上就行。 循环所有children
    2. 本质是DFS
  3. Word Squares
    1. 还是搜索 DFS
    2. 第一行确定同时第一列也确定,第二行确定同时第二列也确定。。。
    3. 剪枝 看所有已确定前缀的是不是都有此前缀
    4. 用Trie
  4. Word Search II

 

Chapter 2-2 Segment Tree

 

Chapter 3 Data Structure II - Stack, Heap                    

堆 Heap

支持操作 O(1) Min/Max / log(N) Push / log(N) Pop

priority_queue: O(N) remove

  1. Trapping Rain Water
    1. 总共的水 = 每个位置能盛住的水的总和
    2. 每个位置能盛住的水取决于: 他自己的高度;左边的右边的最高高度的最小值(也把自己考虑进去)
    3. O(1) space: two pointers 相向而行
    4. 左指针记录从左到当前位置最大值,右指针记录从右到当前位置最大值
    5. 比较出较小的值,当前位置的值 = 较小值 - 当前值
    6. 较小的值的指针挪一格
  2. Trapping Rain Water II
    1. 从外往内遍历 + heap
    2. 先从最外一圈围墙找最小值(放入堆中(高度,坐标))标记访问
    3. 去掉最小值,围墙往里凹(上下左右方向),凹进去的值入堆  最小值-当前高度 =(盛下的水)
  3. Data Stream Median
    1. O(nlogn) 左边maxheap,右边minheap
    2. 定义median为左边集合最大值
    3. 2个集合,<= median ,>=median 
    4. 一边x,x或者 x+1,x 个元素
    5. 左边部分最大值就是median
    6. 新来一个元素,如果x+1,x给右边,如果x,x给左边
    7. 这样以后看看是不是左边都小于右边(左边最大值大于右边最小值)=>交换
  4. Sliding Window Median
    1. O(nlogn)
    2. multiset: 平衡二叉树 O(logn)插入删除,O(1)取最大最小 且内容有序
    3. 中位数怎么想到堆:存左半边较小的数们和右半边较大的数们
    4. Sliding Window的题目可以拆解为下面两步
      1. 加一个元素
      2. 删一个元素

栈 Stack

解决数组问题的常用数据结构之一。

支持操作:O(1) Push / O(1) Pop / O(1) Top

  1. Min Stack
    1. push, pop, top, and retrieving the minimum element in constant time
    2. pop()上一个push进去的数
    3. 用两个stack,同步第二个stack记录第一个stack值时的最小值
  2. Expression Expand (decode string)
    1. 用栈,碰到右括号时候做处理:从右往左一直pop到左括号
    2. 记录括号内的东西,记录括号前的数字 
    3. 转化完毕后push进去

单调栈 Monotonous stack

要注意栈中存的是value还是index

栈中只保存升序序列
新元素插入前 pop 掉所有比它大的
stack([1,2,8,10]).push(5) => stack([1,2,5])

  1. Largest Rectangle in Histogram
    1. 找左边第一个比自己小,找右边第一个比自己小的
    2. 单调栈 右边来一个第一个比自己小的,会被pop出去,左边的肯定比自己小
    3. 所以面积是到自己被pop出去时计算到左到右(所以要一个记录每一个进栈的index->单调栈记录index)
    4. O(n)
  2. Maximum Rectangle
    1. 每一行求直方图最大矩阵,每一行的高度都是连续true到顶有多少个
    2. O(mn)
  3. Max Tree
    1. 单调递减栈 O(n)
    2. 在左边是左儿子,在右边是右儿子
    3. 栈的第一个永远是root,第二个就是第二大的因此为成为root右maxtree的root,以此类推。。
数据结构 
Heappriority_queue

Hash

unordered_map
Balanced BSTMap
Balanced BSTSet
有序哈希表N/A

 

Chapter 4 Sweep Line, Deque, Binary Search Hard

Sweep Line扫描线算法

区间问题巧妙解法:

  • 统计当前扫描线跨越的区间有哪些
  • N个区间,2N个变化
  • 起始+1,终止-1

扫描问题的特点

  1. 事件往往是以区间的形式存在
  2. 区间两端代表事件的开始和结束
  3. 按照区间起点排序,起点相同的按照终点排序

扫描线要点

将起点和终点打散排序  (和上面说的起点,终点排序不一样)

[[1,3], [2,4]] => [[1,start],[2,start],[3,end],[4,end]]

  1. number of airplanes in the sky
    1. 起点[1,1],终点[3,-1]
  2. Building Outline (the skyline problem)
    1. 扫描线扫过区间的最高高度
    2. 维护一个集合支持加入,删除任意,求最大
    3. 用set
    4. 重复的key怎么办(每个建筑给一个index)
  3. meeting rooms II
    1. same as number of airplanes in the sky
  4. time intersection
    1. same as number of airplanes in the sky

 

二分法难题

  1. Find Peak Element
    1. O(logn)
  2. Find Peak Element II
    1. 二分找到一列。n,m问题在O(n)时间变成n,m/2问题 => O(nlogm)
      1. O(n)找到一列中的最大点,看他左边点和右边点和他的关系
      2. 如果左边大,意味着爬山法不会从左边爬到右边去=>往左边爬
      3. 右边大=>往右边爬
      4. 都大,随便爬
    2. 进一步优化O(m+n):
      1. 行列交替二分:m,n -> m/2,n->m/2,n/2->m/4,n/2
      2. 列找出最大值之后的某一侧行进行二分
      3. 继续进行

二分答案

Binary Search on Result

往往没有给你一个数组让你二分

同样是找到满足某个条件的最大或者最小值

解题方法

通过猜值判断是否满足题意不对去搜索可能解

  1. 找到可行解范围
  2. 猜答案
  3. 检验条件
  4. 调整搜索范围
# 1.确定答案范围
start,end = 答案可能的值域范围

while start + 1 < end:
    #猜答案
    mid = (start+end)//2

    if should_be_smaller(mid):    #3.检测答案
        end = mid
    else:
        start = mid

最后在检测 start 和 end 谁是我要的答案
  1. First bad version
  2. Sqrt(x)
  3. Sqrt(x) II
    1. 数是double
    2. 一直二分直到|number^2 - x|<=1e^-10
  4. Wood Cut
    1. 可能的piece x/a+y/a+z/a
    2. 可能的长度: 解的范围1~max(x+y+z)
    3. 二分 x/a+y/a+z/a >= k
    4. O(nlog(maxL))
  5. Copy Books
    1. 二分答案
      1. 限制每个抄写员最多抄k页书,需要多少个抄写员
      2. 范围最少需要最大页数的书。最多是所有的书
      3. 和规定的抄写员个数比较
      4. O(nlogsum)
    2. DP:
      1. f[i][j]表示前i本书分给j个人的最少的完工时间
      2. f[i][j] = max(f[x][j-1],sum(x+1,i))
      3. 所有的方案取min
      4. O(n*k*n)
  6. Find The Duplicate Number
    1. 二分答案
      1. First number that smaller_than_or_equal_to(number) > number
      2. O(nlogn)
      3. 1到n之间的数二分
    2. 快慢指针
  7. Maximum Average subarray II
    1. 转化为maximum subarray >= k size(prefix sum)
    2. 二分答案,所有数最小值到所有数最大值
    3. 数组中减去二分猜的那个值然后求maximum subarray(>= k size)
    4. 如果有大于等于0的则往上二分,小于0往下二分
    5. 最后 start-end < 某个小值,停下

 

Deque双端队列

Deque:两端都会有push 和 pop

  1. Sliding Window Maximum
    1. inqueue 从deque右侧进来
    2. 右侧来一个比deque back小的,直接存进
    3. 右侧来一个比deque back大的,pop back 直到空或者有一个不比要进来的小
    4. outqueue从deque左侧出去,只有要删的和deque front的那个数字一样才会出队
    5. 综上,维护一个候选可能的最大值集合

思路总结

二分法

  • 按值二分,需要怎么二分性

扫描线

  • 见到区间需要排序就可以考虑扫描线

双端队列

  • 只用掌握sliding windows maximum这一道题目
  • 维护一个候选可能的最大值集合

 

Chapter 5 DP: Rolling Array & Memoization

今天我们会接触到如下的几种动态规划类型:

  1. 矩阵型
  2. 博弈型
  3. 区间型

动态规划的4点要素

  1. 状态State
    • 灵感,创造力,存储小规模问题的结果
      • 最优解/Maximum/Minimum
      • Yes/No
      • Count(*)
  2. 方程Function
    1. 状态之间的联系,怎么通过小的状态,来求得大的状态
  3. 初始化Intialization
    1. 最极限的小状态是什么,起点
  4. 答案Answer
    1. 最大的那个状态是什么,终点

 

滚动数组- DP空间优化神器 (rolling array)

Fibonacci

传统写法1:f[i] = f[i - 1] + f[i - 2]

传统写法2:c = a + b; a = b; b = c;

滚动数组写法:f[i % 3] = f[(i - 1) % 3] + f[(i - 2) % 3]

请问%2是否可行?可

 

一维滚动数组

  1. House Robber 
    1. 最大最小,数字固定, 用DP
    2. f[i]表示前i个数的最优值
    3. 分成拿第i个数;不拿第i个数
    4. f[i] = max{f[i-2] + A[i], f[i-1]}
  2. House Robber II
    1. 房屋呈环状
    2. 前n-1求不是环状,再后n-1求不是环状。2遍动态规划
    3. 意思是第一家不抢,后面随便抢。或者最后一家不抢,前面随便抢 

Fibonacci 类问题:

  1. climbing stairs
  2. fibonacci

二维滚动数组

  1. Maximal Square
    1. 正方形可以进行递推
    2. f[i][j] i,j这个位置能填出最大正方形的边长
    3. f[i%2][j] = min{f[(i-1)%2][j],f[(i-1)%2][j-1],f[i%2][j-1]}+1 ifA[i][j] == 1 else 0
    4. 初始化 f[0][i] = matrix[0][i], f[i%2][0] = matrix[i][0]
    5. 滚动数组只降一维
  2. Maximal Square II
    1. f[i][j] = min{f[i-1][j-1] 和f[i-1][j]往左有几个0和f[i][j-1]往上有几个0}+1

矩阵类的题目

  1. 正方形用右下角作为定位角
  2. 长方形可以用左上角右下角作为定位角

二维动态规划空间优化

这类题目特点

f[i][j] =由f[i-1]行来决定状态,

第i行跟i-1行之前毫无关系,

所以状态转变为

f[i%2][j] =由f[(i-1)%2]行来决定状态

二维滚动数组相关问题

  1. unique paths
  2. minimum path sum
  3. edit distance

 

记忆化搜索 (填表的复杂度)

分治法搜索+三行+return变动态规划

  • 本质上:动态规划
  • 动态规划就是解决了重复计算的搜索
  • 动态规划的实现方式:
    • 循环(从小到大递推)
    • 记忆化搜索(从大到小搜索)
      • 画搜索树
      • 万金油
  1. Longest Continuous Increasing Subsequence ii
    1. DFS+memo
    2. 那怎么根据 DP 四要素转化为记忆化搜索呢?
      1. State:
        1. memo[x][y] 以x,y作为开头的最长子序列
      2. Function:
        1. 遍历 x,y 上下左右四个格子 (x_, y_)
        2. memo[x][y] = max(memo[x][y], memo[x_][y_] + 1)
          (if A[x][y] < A[x_][y_])
      3. Intialization:
        1. memo[x][y] 是极小值时,初始化为1
      4. Answer:
        1. memo[x][y]中最大值

什么时候用记忆化搜索?

  1. 状态转移特别麻烦,不是顺序性
    1. Longest Continuous Increasing Subsequence II
      • 遍历 x,y 上下左右四个格子
    2. Coins in a Line III
      • dp[i][j] = sum[i][j] - min(dp[i+1][j], dp[i][j-1]);
  2. 初始化状态不是很容易找到
    1. Longest Continuous Increasing Subsequence II
      • 初始化极小值
    2. Coins in a Line III
      • 初始化dp[i][i]
  3. 从大到小

记忆化搜索的缺陷

  • 耗费更多空间,无法使用滚动数组优化
  • 递归深度可能会很深

 

博弈类动态规划 Game DP

Minimax Game,对手选择optimal,我方选择也是optimal

memo search, at current state, search for all possible next state and pass to the recursion call the next possible state, and check if the opponent cannot win, then at current state I can win. If from all next states the opponent can win, then at current state I lose.

博弈有先后手

  • State:
    • 先手是否能获胜/能够获得的最大利益
  • Function:
    • 循环枚举先手的策略可能性
  • Intialization:
    • 最极限/最小的状态下的先手的值
  • Answer:
    • 整个问题先手是否可能获胜

先思考最小状态

然后思考大的状态->往小的递推,那么非常适合记忆化搜索

  1. Coins in a line
    1. f[i]硬币个数是i时先手是否必胜
    2. f[i-1] f[i-2]有一个先手必败,则必胜
    3. Intialization:
      • dp[0] = false
      • dp[1] = true
      • dp[2] = true
  2. Coins in a Line II
    1. State:
      • dp[i] 表示取 i ~ n-1 这些硬币时,先手取得的最大硬币总价
    2. Function:
      • sum[i] 是后 i ~ n-1 这些硬币的价值总和
      • dp[i] = max(
      • sum[i] - dp[i + 1], # 先手取一个硬币
      • sum[i] - dp[i + 2], # 先手取两个硬币
      • )
    3. Intialization:
      • dp[n-1] = coin[n-1]
      • dp[n-2] = coin[n-1] + coin[n-2]
    4. Answer:
      • dp[0]
  3. Coins in a Line III
    1. 区间型动态规划
    2. State:
      • dp[i][j] 现在还第i到第j的硬币,先手可以最多取走的硬币总价值
    3. Function:
      • sum[i][j] 表示第i到第j的硬币价值总和
      • dp[i][j] = max(sum[i][j] - dp[i+1][j], sum[i][j] - dp[i][j-1]);
    4. Intialize:
      • dp[i][i] = coins[i]
    5. Answer:
      • dp[0][n-1]

今日重点三题

  • House Robber
    • 滚动数组优化最简单的入门。
  • Longest Increasing continuous Subsequence 2D
    • 记忆化搜索的经典题,此题只有记忆化搜索才能最优。
  • Coins in a Line III
    • 博弈问题和记忆化搜索的结合

 

Chapter 6 动态规划(下) -区间、匹配与背包

  • 区间类DP
    • Stone Game
    • Burst Ballons
    • Scramble String *
  • 匹配类DP
    • Longest Common Subsequence
    • Edit Distance
    • K Edit Distance *
    • Distinct Subquence *
    • Interleaving String *
  • 背包类DP
    • BackPack I
    • BackPack II
    • K SUM

 

区间类DP

特点:

  1. 求一段区间的解max/min/count
  2. 转移方程通过区间更新
  3. 大区间的值依赖于小区间

区间动态规划的三种实现方式

  1. 先循环区间长度,再循环起点位置 (环形的比较容易做?)
    for(int l = 2; l <= n; l++){   //操作区间的长度
        for(int i = 0; i + l <= n; i++){ 
            int j = i + l - 1;
            // now we get(i,j)
            for (int k = i; k < j; k++) {
                //update
            }
        }
    }
    

     

  2. 起点倒过来循环,终点正过去循环
    for(int i = n-1; i >=0; i--){
        for(int j = i; j < n; j++){
            // now we get (i,j)
            for (int k = i; k < j; k++) {
                //update
            }
        }
    }

     

  3. 记忆化搜索
def memo_search(self,A,i,j,range_sum,memo):
    if i==j:
        return 0

    if(i,j) in memo:
        return memo[(i,j)]
    
    score = sys.maxsize
    for k in range(i,j):
        left = self.memo_search(A,i,k,range_sum,memo)
        right = self.memo_search(A,k+1,j,range_sum,memo)
        score = min(score,left + right + range_sum[i][j])

    memo[(i,j)] = score
    return score
  1. Stone game
    1. 记忆化搜索的思路,从大到小,先考虑最后的0-n-1 合并的总花费
    2. dp[i][j]: i到j石子合并到一堆的最小花费 O(n^3)
    3. Function:
      • 预处理sum[i,j] 表示i到j所有石子价值和
      • dp[i][j] = min(dp[i][k]+dp[k+1][j]+sum[i,j]) 对于所有k属于{i,j-1}
    4. Intialize:
      • For each i
      • dp[i][i] = 0
    5. 决策单调-> O(n^2)
  2.  Burst Ballons
    1. 死胡同:容易想到的一个思路从小往大,枚举第一次在哪吹爆气球?
      记忆化搜索的思路,从大到小,先考虑最后一个气球被吹爆之后的获益
    2. 将原数组两端加上1
      • [4,3,5] => [1,4,3,5,1]
      • 数组长度 n 从 3 变为 5
    3. State:
      • dp[i][j] 表示把第i+1到第j-1个气球打爆,剩下i,j的最大收益
    4. Function:
      • 对于所有k属于{i + 1,j - 1}, 表示第k号气球最后打爆。
      • score = arr[i] * arr[k] * arr[j]
      • dp[i][j] = max(dp[i][k]+dp[k][j]+score)
    5. Intialization:
      • dp[i][i] = 0
    6. Answer:
      • dp[0][n-1]
  3.  Scramble String
    1. State:
      • dp[x][y][k] 表示是从s1串x开始,s2串y开始,他们后面k个字符组成的substr是Scramble String
    2. Function:
      • 对于所有i属于{1,k}
      • s11 = s1.substring(0, i); s12 = s1.substring(i, s1.length());
      • s21 = s2.substring(0, i); s22 = s2.substring(i, s2.length());
      • s23 = s2.substring(0, s2.length() - i); s24 = s2.substring(s2.length() - i, s2.length());
      • for i = x -> x+k
      • dp[x][y][k] = (dp[x][y][i] && dp[x+i][y+i][k-i]) || dp[x][y+k-i][i] && dp[x+i][y][k-i])
    3. Intialize:
      • dp[i][j][1] = s1[i]==s[j].
    4. Answer:
      • dp[0][0][len]

区间DP总结

  • 最后都是求0~n-1这段区间的值
  • 逆向思维:考虑最后一步如何做,而不考虑第一步如何做
  • 使用记忆化搜索更容易实现

 

匹配型动态规划

  • 匹配两个字符串的最优值/方案数/可行性
  • 可以使用滚动数组优化空间

方法:

  • state: f[i][j]代表了第一个sequence的前i个数字/字符,配上第二个sequence的前j个...
  • function: f[i][j] =研究第i个和第j个的匹配关系(实际上只需研究“上”,“左”,“左上”三个状态)
  • initialization: f[i][0]和f[0][i]
  • answer: f[n][m] min/max/数目/存在关系
  • n = s1.length()
  • m = s2.length()

解题技巧: 画矩阵,填写矩阵,之前不要忘记空串

  1. Longest Common Subsequence
    1. state: f[i][j]表示前i个字符配上前j个字符的LCS的长度
    2. function: f[i][j] = max(f[i-1][j], f[i][j-1], f[i-1][j-1] + 1) // A[i - 1] == B[j - 1]
      • = max(f[i-1][j], f[i][j-1]) // A[i - 1] != B[j - 1]
    3. intialize: f[i][0] = 0 f[0][j] = 0
    4. answer: f[n][m]
  2. Edit Distance
    1. state: f[i][j]表示A的前i个字符最少要用几次编辑可以变成B的前j个字符
    2. function: f[i][j] = min(f[i-1][j]+1, f[i][j-1]+1, f[i-1][j-1]) // A[i - 1] == B[j - 1]
      • = min(f[i-1][j]+1, f[i][j-1]+1, f[i-1][j-1]+1) // A[i - 1] != B[j - 1]
    3. initialization: f[i][0] = i, f[0][j] = j
    4. answer: f[n][m]
  3. Distinct Subsequence
    1. state: f[i][j] 表示 S的前i个字符中选取T的前j个字符,有多少种方案
    2. function: f[i][j] = f[i - 1][j] + f[i - 1][j - 1] // S[i-1] == T[j-1]
      • = f[i - 1][j] // S[i-1] != T[j-1]
    3. initialize: f[i][0] = 1, f[0][j] = 0 (j > 0)
    4. answer: f[n][m] (n = sizeof(S), m = sizeof(T))
  4. Interleaving String
    1. state: f[i][j]表示s1的前i个字符和s2的前j个字符能否交替组成s3的前i+j个字符
    2. function: f[i][j] = (f[i-1][j] && (s1[i-1]==s3[i+j-1]) ||
      • (f[i][j-1] && (s2[j-1]==s3[i+j-1])
    3. initialize: f[i][0] = (s1[0..i-1] == s3[0..i-1])
      • f[0][j] = (s2[0..j-1] == s3[0..j-1])
    4. answer: f[n][m], n = sizeof(s1), m = sizeof(s2)
  5. K Edit Distance

 

背包类DP

背包问题状态转移方程的推导核心是:分类 要第i件物品 与 不要第i件物品 两种情况,
若可以挑多件,那就是分挑 0,1,2, ...., k件那么多种情况。

特点:

  • 用值作为DP维度
  • DP过程就是填写矩阵
  • 可以滚动数组优化
  1. Backpack
    1. State:
      • f[i][S] “前i”个物品,取出一些能否组成和为S
    2. Function:
      • a[i-1] 是第i个物品下标是i-1
      • f[i][S] = f[i-1][S - a[i-1]] or f[i-1][S]
    3. Intialize:
      • f[i][0] = true;
      • f[0][1..target] = false
    4. Answer:
      • 检查所有的f[n][j]
    5. O(n*S) , 滚动数组优化
    6. 区间合并O(n)
  2. Backpack II
    1. 状态 State
      • f[i][j] 表示前i个物品当中选一些物品组成容量为j的最大价值
    2. 方程 Function
      • f[i][j] = max(f[i-1][j], f[i-1][j-A[i-1]] + V[i-1]);
    3. 初始化 Intialization
      • f[0][0]=0;
    4. 答案 Answer
      • f[n][s]
    5. O(n*s)
  3. Backpack IV
    1. State:
      • f[i][j] “前i”个物品,取出一些物品,第i物品随便取多少个,组成和为j的个数
    2. Function:
      • a[i-1] 是第i个物品,下标是i-1
      • k 是第i个物品选取的次数
      • f[i][j] = sum(f[i-1][j - k*a[i-1]])
    3. Intialization:
      • f[i][0] = true; f[0][1..target] = false
    4. Answer:
      • 答案是f[n][target]
    5. 进一步优化方程:
      • f[i][j] = f[i - 1][j] + f[i][j - a[i - 1]]
  4. K sum
    1. n个数,取k个数,组成和为target
    2. State:
      • f[i][j][s]前i个数取j个数出来能否和为s
    3. Function:
      • f[i][j][s] = f[i - 1][j - 1][s - a[i-1]] + f[i - 1][j][s]
    4. Intialization
      • f[i][0][0] = 1
    5. Answer
      • f[n][k][target]
  5. Minimum Adjustment cost
    1. State:
      • f[i][v] 前i个数,第i个数调整为v,满足相邻两数<=target,所需要的最小代价
    2. Function:
      • f[i][v] = min(f[i-1][v’] + |A[i]-v|, |v-v’| <= target)
    3. Answer:
      • f[n][a[n]-target~a[n]+target]
    4. O(n * A * T)

Summary

  • 区间类DP问题
    • 从大到小去思考
    • 主要是通过记忆化来直观理解DP的思路
  • 匹配类DP问题
    • 二维数组
    • 画出矩阵的表格,填写矩阵
    •  可以滚动数组优化
  • 背包DP问题
    • 用值作为DP维度
    • DP过程就是填写矩阵
    • 可以滚动数组优化

重点题型

  • Stone-Game
    • 区间类DP的入门题
  • Edit Distance
    • 匹配常考题
  • Backpack II
    • 有价值的背包题目才有价值

DP问题有多少限制就开多少数维组

 

Chapter 7 面试中较难的 Follow Up 问题

  • Subarray sum 3 follow up
  • Continuous Subarray Sum 2 follow up
  • Wiggle Sort 2 follow up
  • Partition 3 follow up
  • Iterator 3 follow up

Subarray sum

  1. Subarray sum
    1. prefixsum, hash table
  2. Subarray Sum Closest
    1. set,map (upperbound,lowerbound)
    2. sort prefixsum
  3. Submatrix Sum
    1. 压缩上下边界 O(n^2)
    2. 再用subarray sum
  4. Subarray Sum II
    1. O(n^2) 前缀和
    2. O(nlogn)二分前缀和
    3. O(n)前缀和 + 同向双指针

 

Continuous Subarray Sum

  1. Continuous Subarray Sum
    1. maximum subarray
  2. Continuous Subarray Sum II
    1. maximum subarray + total - minimum subarray
    2. minimum subarray, 每个数*-1 算maximum subarray 再取反

 

Partition

1. Quick select

Kth Largest

PriorityQueue

  • 时间复杂度O(nlogk)
  • 更适合Topk
  • 更适合实时

min heap O(nlogn + klogn)

max heap maintain a maxheap of size k O(nlogk)

QuickSelect

  • 时间复杂度O(n)
  • 更适合第k大

 

Partition问题模板

  • 最坏时间复杂度?
  • 为什么要选择(left + right) / 2作为pivot
  • 为什么pivot既不属于左边也不属于右边 (为了更好均分左右两遍,避免最坏情况:数字都一样)
  • 为什么是left <= right而不是left < right (left和right交错开,严格属于左边或者右边)
left, right = start, end
pivot = nums[(start+end)//2]
while(left <= right):
    while left <= right and nums[left] < pivot:
        left += 1
    while left <= right and nums[right] > pivot:
        right -= 1
    if left <= right:
        nums[left], nums[right] = nums[right], nums[left]
        left, right = left + 1, right - 1

 

Wiggle Sort

  1. wiggle sort
  2. wiggle sort ii
    1. 不用O(n), 用O(nlogn) 先排序,再中分前一堆第i个配第二堆第i个
    2. O(n) 直接partition, find medium(find kth) + 3路 partition
  3. Nuts & Bolts Problem
    1. 如果nuts之间可以比较,bolts之间可以比较,那么就可以分别快速排序。只能比较nuts和bolts,就可以利用一方排序另一方
    2. 先用bolts中的任意一个,B,去将nuts分成三部分:–小于B的nuts;正好对应B的nut;大于B的nuts
    3. 然后用这个中间的nut,N,去将bolts分成三部分:–小于N的bolts;B;大于N的bolts
    4. 分别递归
    5. 平均时间复杂度:O(nlogn)

 

Iterator

必须用stack

  1. flatten list
    1. 遇到整数就塞入结果,遇到list就将本层iterator塞入栈,然后处理list的iterator
  2. Flatten Nested List Iterator
    1. 和FlattenList想法类似,使用栈
    2. 先将List里的元素倒序放入栈,即List第一个元素在栈顶
    3. 每次调用getNext时
      1. 遇到整数输出结果并pop
      2. 遇到list就倒序放入栈,继续,直到遇到整数
    4. List转Stack
    5. 主函数逻辑放在HasNext里面
    6. Next只做一次pop处理
  3. Flatten 2D Vector
    1. 存储Input List的iterator i

    2. i每次后移一位,代表处理下一个一维链表

    3. 元素的iterator为j

      1. j一开始指向null

      2. 每次如果j是null,就后移i,j指向i所指链表的第一个元素

      3. 类似二维for loop

  4. Binary Search Tree Iterator
    1. 这个题是next里找下一个元素,hasNext单纯判断
    2. 用栈模拟中序遍历dfs

    3. 首先将root,root的左儿子,root 的左儿子的左儿子,...依次放入栈

    4. 每次输出栈顶p

      1. 如果栈顶p有右儿子r,将r,r的左儿子,r左儿子的左儿子,...依次放入栈

      2. 如果栈顶p没有右儿子,则不停pop栈顶,直到栈为空,或者刚pop的元素是新任栈顶的左儿子

 

Follow Up常见方式

  • 一维转二维
    • 可以套相同的思路试一试
      • Find Peak Element I/II
      • Trapping Water I/II
      • Subarray Sum/Submatrix Sum
  • 数组变成循环数组
    • 循环数组小技巧
      • Continuous Subarray Sum
  • 题目条件加强
    • 可能题目的解题方法会变化
      • Wiggle Sort I/II
  • 换马甲(变一个描述,本质不变)
    • 本质不变
      • Number of airplane on the Sky/ Meeting Room
      • BackPack Problem
  • 描述完全不一样,但是方法相同
    • 这种题目得去分析
      • 前向型指针的题目
      • Quick Sort/ Bolt Nuts Problem
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值