动态规划

动态规划的三要素

重叠子问题、最优子结构、状态转移方程

明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case。

第⼆个凑零钱的问题,展⽰了如何流程化确定「状态转移⽅程」,只要通过
状态转移⽅程写出暴⼒递归解,剩下的也就是优化递归树,消除重叠⼦问题
⽽已。

计算机解决问题其实没有任何奇技淫巧,它唯⼀的解决办法就是穷举,穷举
所有可能性。算法设计⽆⾮就是先思考“如何穷举”,然后再追求“如何聪明
地穷举”。

动态规划的流程

具体来说,动态规划的一般流程就是三步:

暴力的递归解法 -> 带备忘录的递归解法 -> 迭代的动态规划解法

就思考流程来说,就分为以下三步:

找到状态和选择 -> 明确 dp 数组/函数的定义 -> 寻找状态之间的关系

for 状态1 in 状态1的所有取值:
	for 状态2 in 状态2的所有取值:
		for ...
			dp[状态1][状态2][...] = 择优(选择1,选择2...)

常用的状态转移方程推导技巧

  • 数学归纳法

经典动态规划:高空扔鸡蛋🥚

我们选择在第 i 层楼扔了鸡蛋之后,可能出现两种情况:鸡蛋碎了,鸡蛋没碎。注意,这时候状态转移就来了:
如果鸡蛋碎了,那么鸡蛋的个数 K 应该减⼀,搜索的楼层区间应该从[1…N] 变为 [1…i-1] 共 i-1 层楼;
如果鸡蛋没碎,那么鸡蛋的个数 K 不变,搜索的楼层区间应该从 [1…N]变为 [i+1…N] 共 N-i 层楼。

经典动态规划:0-1背包问题👜

  • 问题描述

    给你⼀个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i] ,价值为 val[i] ,现在让你⽤这个背包装物品,最多能装的价值是多少?

  • 明确状态

    【背包的容量】、【可选择的物品】

  • 明确选择

    【装进背包】 or 【不装进背包】

  • 明确dp数组的定义

    状态两个,因此首选二维数组

    👉👉dp[i][w]的定义如下:对于前i个物品,当前背包的容量为w,这种情况下可以装下的最大价值dp[i][w]

  • 确定base case

    根据对dp数组的定义,我们最终想求得的答案就是dp[N][W]。确定base case dp[0][...] = dp[...][0] = 0

    因为没有物品时或者背包没有空间时,能装下的最大价值就是0

  • 根据【选择】明确逻辑框架

    【装进背包】:如果把这第i个物品装入背包,那么dp[i][w]应等于dp[i-1][w-wt[i-1]] + val[i-1]

    即第i-1个物品留有wt[i-1]重量空间给第i个物品,由于从索引1开始,因此第i对应i-1

    【不装进背包】:如果不把这第i个物品装进背包,则继承之前的结果,那么最大价值dp[i][w] = dp[i-1][w]

    【综合考虑】:dp[i][w]等于【装进背包】和【不装进背包】两者带来最大价值的那个选择;
    d p [ i ] [ w ] = m a x ( d p [ i − 1 ] [ w ] , d p [ i − 1 ] [ w − w t [ i − 1 ] ] + v a l [ i − 1 ] ) dp[i][w] = max(dp[i-1][w],dp[i-1][w-wt[i-1]]+val[i-1]) dp[i][w]=max(dp[i1][w],dp[i1][wwt[i1]]+val[i1])

  • 处理索引越界和边界情况

    if(w - wt[i-1] < 0)
    {
    	//这种情况下等同于不装入背包
    	dp[i][w] = dp[i-1][w];
    }
    
  • 迭代形式

    int knapsack(int W, int N, vector<int> &weight, vector<int> &value){
        vector<vector<int>> dp(N + 1, vector<int>(W + 1));
        //initialize base case
        for (int i = 0; i <= N; i++)
            dp[i][0] = 0;
        for (int j = 0; j <= W; j++)
            dp[0][j] = 0;
        //calculate dp
        for (int i = 1; i <= N; i++)
            for (int j = 1; j <= W; j++)
                if (j - weight[i - 1] < 0)
                    dp[i][j] = dp[i - 1][j];
                else
                    dp[i][j] = max(dp[i - 1][j - weight[i - 1]] + value[i - 1],
                                   dp[i - 1][j]);
        return dp[N][W];
    }
    

经典动态规划:完全背包问题👛

  • 问题描述

    给定不同面额的硬币和一个总金额,写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

    示例:输入:amount = 5,coins = [1 , 2 , 5]

    输出:4

    **解释:**有四种方式可以凑成总金额数5,2+2+1,2+1+1+1,1+1+1+1+1

  • 问题转换

    有⼀个钱包,最⼤金额为 amount ,有⼀系列硬币 coins ,每个硬币的金额为 coins[i] ,每个硬币的数量⽆限。请问有多少种⽅法,能够把钱包恰好装满?

    「完全背包问题」即指的是题中的硬币数量无限制。

  • 明确状态

    【钱包的容量】、【可选择的硬币】

  • 明确选择

    【装进钱包】 or 【不装进钱包】

  • 明确dp数组的定义

    状态两个,因此首选二维数组

    👉👉dp[i][j]的定义如下:只使用前i个硬币,当前钱包的容纳金额为j时,有dp[i][j]种方法能装满钱包!

    若只使⽤ coins 中的前 i 个硬币的⾯值,若想凑出⾦额 j ,有 dp[i][j] 种凑法。

  • 确定base case

    根据对dp数组的定义,我们最终想求得的答案就是dp[N][amount]。其中Ncoins数组的大小,

    确定base case: dp[0][...] = 0,dp[...][0] = 1

    因为没有硬币可以选择时,则无论如何都凑不出指定金额;如果指定金额为0,则空集是唯一解集。

  • 根据【选择】明确逻辑框架

    【装进钱包】:如果把这第i个硬币装入钱包,那么dp[i][j]应等于dp[i-1][j-coins[i-1]]

    即第i-1个物品留有wt[i-1]重量空间给第i个物品,由于从索引1开始,因此第i对应i-1

    【不装进背包】:如果不把这第i个硬币装进背包,则继承之前的结果,那么方法数dp[i][w] = dp[i-1][w]

    【综合考虑】:dp[i][w]等于【装进背包】和【不装进背包】两个选择方法数之和
    d p [ i ] [ w ] = d p [ i − 1 ] [ j ]   +   d p [ i ] [ j − c o i n s [ i − 1 ] ] dp[i][w] = dp[i-1][j]~+~dp[i][j-coins[i-1]] dp[i][w]=dp[i1][j] + dp[i][jcoins[i1]]

  • 处理索引越界和边界情况

    if(j - coins[i-1] < 0)
    {
    	//这种情况下等同于不装入背包
    	dp[i][j] = dp[i-1][j];
    }
    
  • 迭代形式

    int change(int amount, vector<int> &coins){
        int n = coins.size();
        vector<vector<int>> dp(n + 1, vector<int>(amount + 1));
        //initialize base case
        for (int i = 0; i <= n; i++)
            dp[i][0] = 1;
        for (int j = 1; j <= amount; j++)
            dp[0][j] = 0;
        //dp begin
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= amount; j++)
                if (j - coins[i - 1] < 0)
                    dp[i][j] = dp[i - 1][j];
                else
                    dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
        return dp[n][amount];
    }
    

经典动态规划:子集背包问题💼

  • 问题描述

    给定一个只包含正整数和非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

  • 问题转换

    那么对于这个问题,我们可以先对集合求和,得出 sum把问题转化为背包问题:

    ​ 给⼀个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为nums[i] 。现在让你装物品,是否存在⼀种装法,能够恰好将背包装满?

  • 明确状态

    【容量为数组元素和/2】、【可选择的数组元素】

  • 明确选择

    【装进数组】 or 【不装进数组】

  • 明确dp数组的定义

    状态两个,因此首选二维数组

    👉👉dp[i][w]的定义如下:对于前i个数组元素,当前背包的容量为w,若dp[i][w]true则说明恰好可以将背包装满,若dp[i][w]false,则说明不能恰好把背包装满。

  • 确定base case

    根据对dp数组的定义,我们最终想求得的答案就是dp[N][W]。确定base case dp[0][...] = false,dp[...][0] = true

    解释:当没有物品可选择的时候,则装不满背包;当背包容量为0的时候,则已经装满了背包。

  • 根据【选择】明确逻辑框架

    【装进背包】:如果把这第i个物品装入背包,那么dp[i][w]应等于dp[i-1][w-nums[i-1]]

    即第i-1个物品留有nums[i-1]重量空间给第i个物品,由于从索引1开始,因此第i对应i-1

    【不装进背包】:如果不把这第i个物品装进背包,则继承之前的结果,那么最大价值dp[i][w] = dp[i-1][w]

    【综合考虑】:dp[i][w]等于【装进背包】和【不装进背包】两者谁带来最大利益,即取逻辑或的关系;
    d p [ i ] [ w ]   =   d p [ i − 1 ] [ w − n u m s [ i − 1 ] ]    ∣ ∣    d p [ i − 1 ] [ w ] dp[i][w]~=~dp[i-1][w-nums[i-1]] ~~||~~ dp[i-1][w] dp[i][w] = dp[i1][wnums[i1]]    dp[i1][w]

  • 处理索引越界和边界情况

    if(w - nums[i-1] < 0)
    {
    	//这种情况下等同于不装入背包
    	dp[i][w] = dp[i-1][w];
    }
    
  • 迭代形式

    bool canPartition(vector<int> &nums)
    {
        int sum = 0;
        for (const auto &i : nums) sum += i;
        if (sum % 2 == 1) return false;
        int target = sum / 2; //背包问题中的总重量
        int n = nums.size();
        vector<vector<bool>> dp(n + 1, vector<bool>(target + 1));
        //initialize base case
        for (int i = 0; i <= n; i++)
            dp[i][0] = true;
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= target; j++)
                if (j - nums[i - 1] < 0)
                    dp[i][j] = dp[i - 1][j];
                else
                    dp[i][j] = dp[i - 1][j - nums[i - 1]] ||
                               dp[i - 1][j];
        return dp[n][target];
    }
    

经典动态规划:编辑距离🏳

  • 问题描述

    给定两个字符串s1s2,计算出将s1转换成s2所使用的最少操作数。你可以对字符串进行如下三种操作:

    1.插入一个字符

    2.删除一个字符

    3.替换一个字符

    示例:输入:s1 = "horse,"s2="ros"

    输出:3

  • 明确状态

    s1子串长】、【s2子串长】

  • 明确选择

    【跳过(skip)】 、【删除(delete)】、【插入(insert)】、【替换(replace)】

  • 明确dp数组的定义

    状态两个,因此首选二维数组。解决两个字符串的动态规划问题,⼀般都是⽤两个指针i,j分别指向两个字符串的最后,然后⼀步步往前⾛,缩⼩问题的规模。

    👉👉dp[i][j]的定义如下:对于s1的前i个字符与s2的前i个字符,其最小操作数为dp[i][j]

  • 确定base case

    根据对dp数组的定义,我们最终想求得的答案就是dp[s1.len][s2.len]。确定base case dp[0][...] = [...],dp[...][0] = [...]

    解释:s1空与s2的前n个做匹配,需进行n次插入操作;s2空与s1的前m个做匹配,需进行m次插入操作。

  • 根据【选择】明确逻辑框架

    如果两个字符相同:

    【跳过(skip)】:dp[i][j] = dp[i-1][j-1]

    如果两个字符不同:

    【删除(delete)】:dp[i][j] = dp[i-1][j] + 1

    【插入(insert)】:dp[i][j] = dp[i][j-1] + 1

    【替换(replace)】:dp[i][j] = dp[i-1][j-1] + 1

    【综合考虑】:

    字符相同的情况:
    d p [ i ] [ j ]   =   d p [ i − 1 ] [ j − 1 ] dp[i][j]~=~dp[i-1][j-1] dp[i][j] = dp[i1][j1]
    字符不同的情况:
    d p [ i ] [ j ]   =   m i n { d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] , d p [ i − 1 ] [ j − 1 ] }   + 1 dp[i][j]~=~min\{dp[i-1][j],dp[i][j-1],dp[i-1][j-1]\}~+1 dp[i][j] = min{dp[i1][j],dp[i][j1],dp[i1][j1]} +1
    dp[i][j]等于【删除】、【插入】、【替换】三者谁带来最小操作数,即取min关系;

  • 迭代形式

    int iterateDP(const string &s1, const string &s2)
    {
        int m = s1.size(), n = s2.size();
        //create dp arrary
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        //initialize base case
        for (int i = 1; i <= m; i++)
            dp[i][0] = i;
        for (int j = 1; j <= n; j++)
            dp[0][j] = j;
        //calculate dp arrary
        for (int i = 1; i <= m; i++)
            for (int j = 1; j <= n; j++)
                if (s1[i - 1] == s2[j - 1]) //has the same char
                    dp[i][j] = dp[i - 1][j - 1];
                else
                    dp[i][j] = min(min(dp[i - 1][j],
                                       dp[i][j - 1]),
                                   dp[i - 1][j - 1]) + 1;
        return dp[m][n];
    }
    
  • 递归形式

    int recursiveDP(const string &s1, const string &s2, int i, int j)
    {
        if (i == -1) return j + 1;
        if (j == -1) return i + 1;
        if (hash.find({i, j}) == hash.end()) //not found
        {
            if (s1[i] == s2[j])
                hash[ {i, j}] = recursiveDP(s1, s2, i - 1, j - 1);
            else
                hash[ {i, j}] = min(recursiveDP(s1, s2, i - 1, j),
                                    min(recursiveDP(s1, s2, i, j - 1),
                                        recursiveDP(s1, s2, i - 1, j - 1))) + 1;
        }
        return hash[ {i, j}];
    }
    

动态规划博弈:猜数字大小🔢

  • 问题描述

    我们正在玩一个猜数游戏,游戏规则如下:

    我从 1n 之间选择一个数字,你来猜我选了哪个数字。每次你猜错了,我都会告诉你,我选的数字比你的大了或者小了。然而,当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。直到你猜到我选的数字,你才算赢得了这个游戏。

    示例:

    n = 10, 我选择了8.
    
    第一轮: 你猜我选择的数字是5,我会告诉你,我的数字更大一些,然后你需要支付5块。
    第二轮: 你猜是7,我告诉你,我的数字更大一些,你支付7块。
    第三轮: 你猜是9,我告诉你,我的数字更小一些,你支付9块。
    
    游戏结束。8 就是我选的数字。
    
    你最终要支付 5 + 7 + 9 = 21 块钱。
    

    给定 **n ≥ 1,**计算你至少需要拥有多少现金才能确保你能赢得这个游戏。

  • 明确状态

    【猜数区间】

  • 明确选择

    【猜数区间内的数字】

  • 明确dp数组的定义

    猜数区间为闭区间,有左侧和右侧两个状态,因此首选二维数组。解决区间的动态规划问题,⼀般都是⽤两个指针i,j分别指向数组构成区间,由小至大扩大区间规模。

    👉👉dp[i][j]的定义如下:对于区间[i,j]的最小支付金额为dp[i][j]

  • 确定base case

    根据对dp数组的定义,我们最终想求得的答案就是dp[1,N]。确定base case : dp[i][i] = 0 (i = 1,2,...,N)

    解释:只有一个数时,猜测代价为0

  • 根据【选择】明确逻辑框架

    如果猜测区间左端点k

    dp[i][j] = k + dp[k+1][j]

    如果猜测区间右端点k

    dp[i][j] = k + dp[i][k-1]

    如果猜测区间内点k

    dp[i][j] = k + min(dp[k+1][j],dp[i][k-1])

    【综合考虑】:

    区间端点:
    左 : d p [ i ] [ j ] = k + d p [ k + 1 ] [ j ]     右 : d p [ i ] [ j ] = k + d p [ i ] [ k − 1 ] 左:dp[i][j] = k + dp[k+1][j] ~~~右:dp[i][j] = k + dp[i][k-1] :dp[i][j]=k+dp[k+1][j]   :dp[i][j]=k+dp[i][k1]
    区间内部:
    d p [ i ] [ j ] = m i n { k + max ⁡ i < k < j { d p [ i ] [ k − 1 ] , d p [ k + 1 ] [ j ] } } dp[i][j]{\rm{ }} = min\left\{{\rm{ }}k{\rm{ }} + {\rm{ }}\mathop {\max }\limits_{i < k < j} \left\{ {dp[i][k - 1],dp[k + 1][j]} \right\}\right\} dp[i][j]=min{k+i<k<jmax{dp[i][k1],dp[k+1][j]}}
    dp[i][j]等当前猜测数值k加上左区间或右区间的最大猜测代价;

  • 根据【逻辑框架】列出表格

    [i , j]012345
    0000000
    1001247
    200236
    30034
    4004
    500
  • 迭代形式

    int getMoneyAmount(int n){
        vector<vector<int>> dp(n + 1, vector<int>(n + 1));
        //initialize base case
        for (int i = 1; i <= n; i++)
            dp[i][i] = 0;
        for (int i = n - 1; i >= 1; i--){
            for (int j = i + 1; j <= n; j++){
                int dpMax = INT_MAX;
                for (int k = i; k <= j; k++){
                    if (k == i)
                        dpMax = min(dpMax, k + dp[k + 1][j]);
                    else if (k == j)
                        dpMax = min(dpMax, k + dp[i][k - 1]);
                    else
                        dpMax = min(dpMax, k + max(dp[i][k - 1], dp[k + 1][j]));
                }
                dp[i][j] = dpMax;
            }
        }
        return dp[1][n];
    }
    

动态规划博弈模板:先后手之石子游戏✊

  • 问题描述

    亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i]

    游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。

    亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。

    假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false

    示例:

    输入:[5,3,4,5]
    输出:true
    解释:
    亚历克斯先开始,只能拿前 5 颗或后 5 颗石子 。
    假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
    如果李拿走前 3 颗,那么剩下的是 [4,5],亚历克斯拿走后 5 颗赢得 10 分。
    如果李拿走后 5 颗,那么剩下的是 [3,4],亚历克斯拿走后 4 颗赢得 9 分。
    这表明,取前 5 颗石子对亚历克斯来说是一个胜利的举动,所以我们返回 true 。
    
  • 明确状态

    【猜数区间】

  • 明确选择

    【猜数区间内的数字】

  • 明确dp数组的定义

    猜数区间为闭区间,有左侧和右侧两个状态,因此首选二维数组。解决区间的动态规划问题,⼀般都是⽤两个指针i,j分别指向数组构成区间,由小至大扩大区间规模。

    👉👉dp[i][j]的定义如下:对于区间[i,j]的最小支付金额为dp[i][j]

  • 确定base case

    根据对dp数组的定义,我们最终想求得的答案就是dp[1,N]。确定base case : dp[i][i] = 0 (i = 1,2,...,N)

    解释:只有一个数时,猜测代价为0

  • 根据【选择】明确逻辑框架

    如果猜测区间左端点k

    dp[i][j] = k + dp[k+1][j]

    如果猜测区间右端点k

    dp[i][j] = k + dp[i][k-1]

    如果猜测区间内点k

    dp[i][j] = k + min(dp[k+1][j],dp[i][k-1])

    【综合考虑】:

    区间端点:
    左 : d p [ i ] [ j ] = k + d p [ k + 1 ] [ j ]     右 : d p [ i ] [ j ] = k + d p [ i ] [ k − 1 ] 左:dp[i][j] = k + dp[k+1][j] ~~~右:dp[i][j] = k + dp[i][k-1] :dp[i][j]=k+dp[k+1][j]   :dp[i][j]=k+dp[i][k1]
    区间内部:
    d p [ i ] [ j ] = k + max ⁡ i < k < j { d p [ i ] [ k − 1 ] , d p [ k + 1 ] [ j ] } dp[i][j]{\rm{ }} = {\rm{ }}k{\rm{ }} + {\rm{ }}\mathop {\max }\limits_{i < k < j} \left\{ {dp[i][k - 1],dp[k + 1][j]} \right\} dp[i][j]=k+i<k<jmax{dp[i][k1],dp[k+1][j]}
    dp[i][j]等当前猜测数值k加上左区间或右区间的最大猜测代价;

  • 根据【逻辑框架】列出表格

    [i , j]012345
    0000000
    1001247
    200236
    30034
    4004
    500
  • 迭代形式

    int getMoneyAmount(int n){
        vector<vector<int>> dp(n + 1, vector<int>(n + 1));
        //initialize base case
        for (int i = 1; i <= n; i++)
            dp[i][i] = 0;
        for (int i = n - 1; i >= 1; i--){
            for (int j = i + 1; j <= n; j++){
                int dpMax = INT_MAX;
                for (int k = i; k <= j; k++){
                    if (k == i)
                        dpMax = min(dpMax, k + dp[k + 1][j]);
                    else if (k == j)
                        dpMax = min(dpMax, k + dp[i][k - 1]);
                    else
                        dpMax = min(dpMax, k + max(dp[i][k - 1], dp[k + 1][j]));
                }
                dp[i][j] = dpMax;
            }
        }
        return dp[1][n];
    }
    

经典动态规划:正则表达式

  • 问题描述

    给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.''*' 的正则表达式匹配。

    '.' 匹配任意单个字符
    '*' 匹配零个或多个前面的那一个元素
    

    所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

    说明:

    s 可能为空,且只包含从 a-z 的小写字母。

    p 可能为空,且只包含从 a-z 的小写字母,以及字符 .*

    示例 1:

    输入:
    s = "aa"
    p = "a*"
    输出: true
    解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
    

    示例 2:

    输入:
    s = "ab"
    p = ".*"
    输出: true
    解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
    

    示例 3:

    输入:
    s = "aab"
    p = "c*a*b"
    输出: true
    解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
    
  • 明确状态

    【A的前i个字符】、【B的前j个字符】

  • 明确选择

    【B的最后字符是正常字符】 、【B的最后字符是.】、【B的最后字符是*

  • 明确dp数组的定义

    状态两个,因此首选二维数组。解决两个字符串的动态规划问题,⼀般都是⽤两个指针i,j分别指向两个字符串的最后,然后⼀步步往前⾛,缩⼩问题的规模。

    👉👉dp[i][j]的定义如下:对于A的前i个字符与B的前j个字符,用dp[i][j]表示是否能匹配。

    其中dp数组需包含空串,方便匹配。

  • 确定base case

    根据对dp数组的定义,我们最终想求得的答案就是dp[m][n],其中m = s.size(),n = p.size()

    确定base case :

    空串与非空正则匹配,则需看情况;

    **空串与空串正则匹配,**则dp[0][0] = TRUE

    非空串与空正则匹配,则dp[...][0] = FALSE

    非空串与非空正则匹配,则需看情况;

  • 绘制表格

    [i , j]“”c*a*b
    “”TFTFTF
    aFFFTTF
    aFFFFTF
    bFFFFFT
  • 根据【选择】明确逻辑框架

    【B的最后字符是正常字符】:能匹配则A[i] == B[j] , dp[i][j] = dp[i-1][j-1]

    【B的最后字符是.】:直接匹配A[i] === B[j] , dp[i][j] = dp[i-1][j-1]

    【B的最后字符是*】:则分为看和不看*两种情况:

    不看*:直接砍掉正则串的后面两个, dp[i][j] = dp[i][j−2]

    *:则正则串不动,主串前移一步,dp[i][j] = dp[i-1][j]

    【综合考虑】:

    正常匹配:
    d p [ i ] [ j ]   =   d p [ i − 1 ] [ j − 1 ] dp[i][j]~=~dp[i-1][j-1] dp[i][j] = dp[i1][j1]
    通配符*匹配:
    d p [ i ] [ j ]   =   d p [ i ] [ j − 2 ]   ∣ ∣   d p [ i − 1 ] [ j ] dp[i][j]~=~dp[i][j-2]~||~dp[i-1][j] dp[i][j] = dp[i][j2]  dp[i1][j]
    其中,或运算符两侧的情况需分别讨论,右侧条件更加苛刻,需满足p[j-2] == s[i-1] || p[j-2] == '.'方可。

  • 迭代形式

    bool isMatch(string s, string p)
    {
        int m = s.size();
        int n = p.size();
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));
        //initializing base case
        dp[0][0] = true;//空串与空正则匹配
        for (int i = 1; i <= m; i++)
            dp[i][0] = false;//非空串与空正则不匹配
        //beginning dp
        for (int i = 0; i <= m; i++){
            for (int j = 1; j <= n; j++){
                if (i > 0 && (s[i - 1] == p[j - 1] || p[j - 1] == '.'))//字符匹配
                    dp[i][j] = dp[i - 1][j - 1];//继承之前匹配结果
                else if (p[j - 1] == '*')//通配符*有两种情况
                {
                    //不看*,则正则字符串-2
                    dp[i][j]  =  dp[i][j] || dp[i][j - 2];
                    //看*,则比较非空串与正则串前字符是否匹配
                    if (i > 0 && (s[i - 1] == p[j - 2] || p[j - 2] == '.'))
                        dp[i][j] = dp[i][j] || dp[i - 1][j];
                    //综合两次结果取逻辑或决定
                }
                else
                    dp[i][j] = false;//字符不匹配
            }
        }
        return dp[m][n];
    }
    

动态规划之等差数列划分

  • 绘制表格

    i , j12357
    1TTTFF
    2TTFF
    3TTT
    5TT
    7T

动态规划之回文子串🔤

  • 问题描述

    给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

    具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被计为是不同的子串。

    示例 1:

    输入: "abc"
    输出: 3
    解释: 三个回文子串: "a", "b", "c".
    

    示例 2:

    输入: "aaa"
    输出: 6
    说明: 6个回文子串: "a", "a", "a", "aa", "aa", "aaa".
    

    注意:

    1. 输入的字符串长度不会超过1000。
  • 明确状态

    【字符串开始位置i】、【字符串结束位置j

  • 明确选择

    【子串是回文串】

  • 明确dp数组的定义

    字符串区间为闭区间,有起始位置和结束位置两个状态,因此首选二维数组。解决区间的动态规划问题,⼀般都是⽤两个指针i,j分别指向字符串构成子串区间,由下至上反推问题答案。

    👉👉dp[i][j]的定义如下:对于子串s[i][j]是否构成回文串

  • 确定base case

    根据对dp数组的定义,我们最终想求得的答案就是dp[0,n-1],没有base case。

  • 根据【选择】明确逻辑框架

    如果区间左右字符不同,则不构成回文串dp[i][j] = false

    如果区间左右字符相同,则判断内缩字符串是否构成回文串

    ​ 1) 如果内缩字符串的长度小于2,则必定构成回文串dp[i][j] = true

    ​ 2) 如果内缩字符串的长度大于等于2,则继承内缩串的结果
    d p [ i ] [ j ] = d p [ i + 1 ] [ j − 1 ] dp[i][j] = dp[i+1][j-1] dp[i][j]=dp[i+1][j1]

  • 根据【逻辑框架】列出表格

    [i , j]aabbaa
    aTTFFFT
    aTFFTF
    bTTFF
    bTFF
    aTT
    aT
  • 迭代形式

    int countSubstrings(string s){
        int n = s.size();
        int count = 0;
        vector<vector<bool>> dp(n, vector<bool>(n));
        //beginning dp
        for (int i = n - 1; i >= 0; i--){
            for (int j = i; j < n; j++){
                if (s[i] != s[j])
                    dp[i][j] = false;
                else if ((j - 1) - (i + 1) + 1 < 2)
                    dp[i][j] = true;
                else
                    dp[i][j] = dp[i + 1][j - 1];
                if (dp[i][j])
                    count++;
            }
        }
        return count;
    }
    

经典动态规划:单词拆分🆎

  • 问题描述

    给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

    说明:

    • 拆分时可以重复使用字典中的单词。
    • 你可以假设字典中没有重复的单词。

    示例 1:

    输入: s = "leetcode", wordDict = ["leet", "code"]
    输出: true
    解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
    

    示例 2:

    输入: s = "applepenapple", wordDict = ["apple", "pen"]
    输出: true
    解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
         注意你可以重复使用字典中的单词。
    
  • 明确状态

    【字符串能否用字典单词表示】

  • 明确选择

    【使用字典中的单词】

  • 明确dp数组的定义

    字符串区间为闭区间,用指针i表示右端点,指针j表示左端点,dp[i]数组表示前i个字符能否用字典表示。从左往右,枚举各种情况,能表示即可break返回。

    👉👉dp[i]表示s的前i个字符能否用字典表示。

  • 确定base case

    根据对dp数组的定义,dp[0] = true,因为空串可以在字典中找到(即不使用字典任何单词),我们最终想求得的答案就是dp[n]

  • 根据【选择】明确逻辑框架

    将区间0-i拆分为0-jj-i区间。
    d p [ i ] = d p [ j ] & & c h e c k ( s u b s t r ( j , i − j ) ) dp[i]{\rm{ }} = {\rm{ }}dp[j]{\rm{ }}\& \& {\rm{ }}check(substr(j,i - j)) dp[i]=dp[j]&&check(substr(j,ij))

  • 迭代形式

    bool wordBreak(string s, vector<string> &wordDict){
        set<string> dict;
        for (auto &w : wordDict)
            dict.insert(w);
        vector<bool> dp(s.size() + 1);
        dp[0] = true;
        for (int i = 1; i <= s.size(); i++){
            for (int j = 0; j < i; j++){
                if (dp[j] && dict.find(s.substr(j, i - j)) != dict.end()){
                    dp[i]  = true;
                    break;
                }
            }
        }
        return dp[s.size()];
    }
    

动态规划之戳气球🎈

  • 问题描述

    n 个气球,编号为0n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

    现在要求你戳破所有的气球。如果你戳破气球 i,就可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 leftright 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。

    求所能获得硬币的最大数量。

    • 示例

      输入: [3,1,5,8]
      输出: 167 
      解释: nums = [3,1,5,8] --> [3,5,8] -->   [3,8]   -->  [8]  --> []
           coins =  3*1*5      +  3*5*8    +  1*3*8      + 1*8*1   = 167
      
  • 明确状态

    采用逆向思维,即初始为空,往空数组中逐个插入气球,求区间的最大硬币数。

  • 明确选择

    【先插入的气球编号k

  • 明确dp数组的定义

    此问题为区间内元素的累加或累积问题,一般将区间作为开(闭)区间,用指针i指向左端点,指针j指向右端点,dp[i][j]表示开区间内扎破气球所能获得的最大硬币数。自下而上求解dp数组即可。

    👉👉dp[i][j]表示区间[i][j]最大硬币数。

  • 确定base case

    根据对dp数组的定义,不构成区间或开区间不能插入气球的数组元数为0,因为在该区间内无法插入气球(即原本没有气球可以扎破)。

  • 根据【选择】明确逻辑框架

    KaTeX parse error: Unknown column alignment: * at position 36: …{\begin{array}{*̲{20}{c}} {\math…

  • 迭代形式

        int maxCoins(vector<int> &nums){
            int n = nums.size();
            vector<int> val(n + 2);
            vector<vector<int>> dp(n + 2, vector<int>(n + 2, -1));
            //initializing base case 边界条件(不构成区间)
            for (int i = 1; i <= n; i++)
                val[i] = nums[i - 1];
            val[0] = val[n + 1] = 1;
            //dp begin
            for (int i = n; i >= 0; i--){
                for (int j = i + 1; j < n + 2; j++){
                    if (i - (j - 1) >= 0) dp[i][j] = 0;//不构成能插入气球的区间
                    for (int k = i + 1; k <= j - 1; k++){
                        //枚举区间内每个气球插入
                        int sum = val[i] * val[k] * val[j];
                        sum += dp[i][k] + dp[k][j];//加上左子区间的硬币和右子区间的硬币
                        dp[i][j] = max(dp[i][j], sum);
                    }
                }
            }
            return dp[0][n + 1];
        }
    
  • 递归形式

    	int dfs(int left, int right, const vector<int> &val,vector<vector<int>> &dp)
        {
            if (left >= right - 1) return 0;//不构成区间
            if (dp[left][right] != -1) return dp[left][right];//dp table优化计算
            for (int i = left + 1; i <= right - 1; i++){
                int sum = val[left] * val[i] * val[right];
                sum += dfs(left, i, val, dp) + dfs(i, right, val, dp);//加上左子区间的硬币和右子区间的硬币
                dp[left][right] = max(dp[left][right], res);
            }
            return dp[left][right];
        }
    

动态规划之最长有效括号

  • 问题描述

    给定一个只包含 '('')' 的字符串,找出最长的包含有效括号的子串的长度。

    示例 1:

    输入: "()())"
    输出: 4
    解释: 最长有效括号子串为 "()()"
    

    示例 2:

    输入: "((())"
    输出: 4
    解释: 最长有效括号子串为 "(())"
    

    示例 3:

    输入: "()(())"
    输出: 6
    解释: 最长有效括号子串为 "()(())"
    
  • 明确状态

    【以第i个字符结尾的最长有效括】

  • 明确选择

    【第i字符是左括号()

  • 明确dp数组的定义

    以第i字符结尾的子串是否有效,且构成的最长有效子串为dp[i]

    👉👉dp[i]的定义如下:对于子串s[0] ~ s[i]构成有效括号子串

  • 确定base case

    dp[0] = 0,第一个字符不构成有效括号子串

  • 根据【选择】明确逻辑框架

    如果s[i] = '(',则dp[i] = 0,不构成有效子串

    如果s[i] = ')',则分情况讨论:
    之前有效括号子串加上当前有效括号子串:
    如果上一个字符为'(',则dp[i] = 2 + dp[i-2]
    如果上一个字符为')',则dp[i] = 2 + dp[i-1] + dp[i-dp[i-1]-2]

    索引	0	1	2	3	4	5
    字符	(	)	(	(	)	)
    数组	0	2	0	0	2	4
    

    KaTeX parse error: Unknown column alignment: * at position 33: …{\begin{array}{*̲{20}{c}} 0&{s[i…

  • 迭代形式

        int longestValidParentheses(string s) {
            if (s.empty()) return 0;
            int n = s.size(), maxlen = 0;
            vector<int> dp(n);//索引表示以第i个字符结尾的子串构成有效括号的长度
            //边界条件,无论第一个字符是'('或')',dp[0] = 0;
            dp[0] = 0;
            //动态规划求解maxlen
            for (int i = 1; i < n; i++) {
                if (s[i] == ')') {
                    if (s[i - 1] == '(') {
                        dp[i] = 2 + (i - 2 < 0 ? 0 : dp[i - 2]);
                    } else if (s[i - 1] == ')' && (i - dp[i - 1] - 1) >= 0 
                             && s[i - dp[i - 1] - 1] == '(') {
                        dp[i] = 2 + dp[i - 1] + (i - dp[i - 1] - 2 < 0 ? 0 : dp[i - dp[i - 1] - 2]);
                    }
                }
                else {
                    dp[i] = 0;
                }
                maxlen = max(maxlen, dp[i]);
            }
            return maxlen;
        }
    

贪心

最长递增子序列

  • 问题描述

    给定一个无序的整数数组,找到其中最长上升子序列的长度。

    示例:

    输入: [10,9,2,5,3,7,101,18]
    输出: 4 
    解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
    
  • 贪心+二分实现

    class Solution {
    public:
        int lengthOfLIS(vector<int>& nums) {
            vector<int> v;
            for(int i = 0;i < nums.size();i++){
                if(v.empty() || nums[i] > v.back())//大于末尾元数则入栈
                    v.push_back(nums[i]);
                else
                    v[binarySearch(v,nums[i])] = nums[i];//否则找到大于等于的左边界
            }
            return v.size();
        }
        int binarySearch(const vector<int>& nums,int target){
            int l = 0,r = nums.size() - 1;
            while(l <= r){
                int mid = l + (r - l)/2;
                if(nums[mid] == target) //缩进右边界
                    r = mid - 1;
                else if(nums[mid] < target) //缩进左边界
                    l = mid + 1;
                else if(nums[mid] > target) //缩进右边界
                    r = mid - 1;
            }
            return l;
        }
    };
    

俄罗斯套娃问题

  • 问题描述

    给定一些标记了宽度和高度的信封,宽度和高度以整数对形式 (w, h) 出现。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。

    请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。

    说明:
    不允许旋转信封。

    示例:

    输入: envelopes = [[5,4],[6,4],[6,7],[2,3]]
    输出: 3 
    解释: 最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
    
  • 明确题意

    ​ 该问题为最长递增子序列的二维问题,首先控制一个维度使其为递增序列,再对另一个维度进行最长递增子序列求解,可参照

  • 明确解法

    ​ 首先对一个维度使用sort排序,按递增进行排序;令一个维度若

双指针法

四数之和

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0

为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -228 到 228 - 1 之间,最终结果不会超过 231 - 1 。

例如:

输入:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]

输出:
2

解释:
两个元组如下:
1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

将问题转换为两数之和求解;

    int fourSumCount(vector<int> &A, vector<int> &B, vector<int> &C, vector<int> &D) {
        int n = A.size();
        int sumCount = 0;
        unordered_map<int, int> AandB, CandD;
        //统计两两数组的数字次数
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                AandB[A[i] + B[j]]++;
                CandD[C[i] + D[j]]++;
            }
        }
        //转换为两数之和哈希表法
        for (auto &m : AandB) {
            int tmpCount = 0;
            auto iter = CandD.find(0 - m.first);
            if (iter != CandD.end())
                tmpCount = iter->second * m.second;
            sumCount += tmpCount;
        }
        return sumCount;
    }

四数之和Ⅱ

  • 题目描述

    给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 *a,*b,cd ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

    注意:

    答案中不可以包含重复的四元组。

  • 示例

    给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。
    
    满足要求的四元组集合为:
    [
      [-1,  0, 0, 1],
      [-2, -1, 1, 2],
      [-2,  0, 0, 2]
    ]
    
        vector<vector<int>> fourSum(vector<int> &nums, int target) {
            vector<vector<int>> ans;
            sort(nums.begin(), nums.end());
            int n = nums.size();
            //外部排列组合双指针
            for (int l = 0; l < n; l++) {
                for (int r = n - 1; r > l; r--) {
                    //区间(l,r)内部双指针遍历
                    int front = l + 1, back = r - 1;
                    while (front < back) {
                        int sum = nums[l] + nums[r] + nums[front] + nums[back];
                        if (sum == target) {
                            vector<int> tmp = {nums[l], nums[front], nums[back], nums[r]};
                            ans.push_back(tmp);
                            while (front < back && nums[front] == tmp[1]) front++;//去重
                            while (back > front && nums[back] == tmp[2]) back--;//去重
                        }
                        else if (sum < target) front++;
                        else if (sum > target) back--;
                    }
                    while (r > l && nums[r] == nums[r - 1]) r--;//去重
                }
                while (l + 1 < n && nums[l] == nums[l + 1]) l++;//去重
            }
            return ans;
        }
    

递归编程技巧

计算超级次方

  • **题目:**你的任务是计算 a^b 对 1337 取模,a 是一个正整数,b 是一个非常大的正整数且会以数组形式给出。

  • 示例 1:

    输入: a = 2, b = [3]
    输出: 8
    
  • 示例 2:

    输入: a = 2, b = [1,0]
    输出: 1024
    
  • **求解:**不考虑求模的要求,以 b = [1,5,6,4] 来举例,结合指数运算的法则,我们可以发现这样的⼀个规律:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hty8NFW0-1614683201132)(C:\Users\兰庭信\AppData\Roaming\Typora\typora-user-images\image-20200710231437233.png)]

int base = 1337;
// 计算 a 的 k 次⽅然后与 base 求模的结果
int mypow(int a, int k)
{
	// 对因⼦求模
    a %= base;
    int res = 1;
    for (int _ = 0; _ < k; _++)
    {
	// 这⾥有乘法,是潜在的溢出点
        res *= a;
	// 对乘法结果求模
        res %= base;
    }
    return res;
}
int superPow(int a, vector<int> &b)
{
    if (b.empty()) return 1;
    int last = b.back();
    b.pop_back();
    int part1 = mypow(a, last);
    int part2 = mypow(superPow(a, b), 10);
	// 每次乘法都要求模
    return (part1 * part2) % base;
}

求幂公式除了循环相乘求解之外,还可以通过下列公式进行logN级别的快速求解:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3LtAQCbF-1614683201135)(C:\Users\兰庭信\AppData\Roaming\Typora\typora-user-images\image-20200710231640225.png)]

int base = 1337;
int mypow(int a, int k)
{
    if (k == 0) return 1;
    a %= base;
    if (k % 2 == 1)
    {
	// k 是奇数
        return (a * mypow(a, k - 1)) % base;
    }
    else
    {
	// k 是偶数
        int sub = mypow(a, k / 2);
        return (sub * sub) % base;
    }
}

括号生成

  • 题目:数字n代表生成括号的对数,设计函数生成所有组合;

  • 示例

    输入:n = 2;
    输出:["(())","()()"]
    
  • 思路:回溯法(深度优先搜索+合理剪枝)

    vector<string> ans;
    void backtracking(int l,int r,int n,string s){
    	if(l == n && r == n) ans.push_back(s);
    	if(l < r) return ;
    	if(l < n) dfs(l+1,r,n,s+'(');
    	if(r < n) dfs(l,r+1,n,s+')');
    }
    

二叉树

二叉树的遍历

前序遍历(根左右)

    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> ans;
        vector<TreeNode*> v;
        while(!v.empty() || root != nullptr){
            for(;root != nullptr;root = root->left){
                v.push_back(root);
                ans.push_back(root->val);
            }
            root = v.back()->right;
            v.pop_back();
        }
        return ans;
    }

中序遍历(左根右)

    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> ans;
        vector<TreeNode*> v;
        while(!v.empty() || root != nullptr){
            for(;root != nullptr;root = root->left){
                v.push_back(root);
            }
            ans.push_back(v.back()->val);
            root = v.back()->right;
            v.pop_back();
        }
        return ans;
    }

后序遍历(左右根)

    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> ans;
        vector<TreeNode*> v;
        TreeNode* preNode = nullptr;
        while(!v.empty() || root != nullptr){
            for(;root != nullptr;root = root->left){
                v.push_back(root);
            }
            root = v.back();
            if(root->right == nullptr || root->right == preNode){
                ans.push_back(root->val);
                v.pop_back();
                preNode = root;
                root = nullptr;
            }else{
                root = root->right;
            }
        }
        return ans;
    }

求树节点的和为某个值,一共有多少种可能

int pathSum(TreeNode* root,int target){
	if(root == nullptr) return 0;
	return helper(root,target) + pathSum(root->left,target) + pathSum(root->right,target);
}
int helper(TreeNode* root,int target){
	if(root == nullptr) return 0;
	target -= root->val;
	return target == 0?1:0 + helper(root->left) + helper(root->right);
}

Trie(前缀)树的模板

**概述:**前缀树通过利用字母映射表,在TrieNode* next[26]中保存了对当前结点而言下一个可能出现的所有字符的链接,因此我们可以通过一个父结点来预知它所有子结点的值:

插入:Trie树 中插入一个单词 word。这个操作和构建链表很像。首先从根结点的子结点root开始与 word 第一个字符进行匹配,一直匹配到前缀链上没有对应的字符,这时开始不断开辟新的结点,直到插入完 word 的最后一个字符,同时还要将最后一个结点isEnd = true;,表示它是一个单词的末尾。

**查找:**查找 Trie 树中是否存在单词 word。从根结点的子结点开始,一直向下匹配即可,如果出现结点值为空就返回false,如果匹配到了最后一个字符,那我们只需判断node->isEnd即可。

**前缀匹配:**判断 Trie树 中是或有以 prefix 为前缀的单词。和 查找 操作类似,只是不需要判断最后一个字符结点的isEnd,因为既然能匹配到最后一个字符,那后面一定有单词是以它为前缀的。

通配符匹配:查找 Trie树 中是否存在单词 wordsearch(word) 可以搜索文字或正则表达式字符串,字符串只包含字母 .a-z. 可以表示任何一个字母。利用递归搜索,在遇到.字符时,向下搜索全部节点,对递归结果进行逻辑或运算。

/** 前缀树节点 **/
class TrieNode{
public:
    TrieNode(){
        isEnd = false;
        for (int i = 0; i < 26; i++)
            next[i] = nullptr;
    }
    /** new分配的节点要delete **/
    ~TrieNode(){
        for (int i = 0; i < 26; i++)
            delete next[i];
    }
    bool isEnd;	/**是否单词尾**/
    TrieNode *next[26];
};
/** 前缀树实现 **/
class Trie{
public:
    /** 创建根节点 */
    Trie(){
        root = new TrieNode();
    }
    ~Trie(){
        delete root;
    }
    /** 往根节点插入单词 */
    void insert(string word){
        TrieNode *head = root;
        for (const auto &c : word){
            if (head->next[c - 'a'] == nullptr){
                head->next[c - 'a'] = new TrieNode();
            }
            head = head->next[c - 'a'];
        }
        head->isEnd = true;
    }
    /** 返回前缀是否存在 */
    bool startsWith(string prefix){
        TrieNode *head = root;
        for (const auto &c : prefix){
            if (head->next[c - 'a'] == nullptr)
                return false;
            head = head->next[c - 'a'];
        }
        return true;
    }
        /** 从根节点开始搜索单词 */
    bool search(string word){
        TrieNode *head = root;
        for (const auto &c : word){
            if (head->next[c - 'a'] == nullptr)
                return false;
            head = head->next[c - 'a'];
        }
        return head->isEnd;
    }
    /** 含通配符'.'的递归搜索 */
    bool search(string word){
        return helper(root, word, 0);
    }
    /** 递归搜索 */
    bool helper(TrieNode *head, const string &word, int start){
        if (head == nullptr) return false;
        if (start == word.size()) return head->isEnd;
        bool res = false;
        if (word[start] == '.'){
            for (int i = 0; i < 26; i++){
                res = res || helper(head->next[i], word, start + 1);
            }
        }
        else{
            res = helper(head->next[word[start] - 'a'], word, start + 1);
        }
        return res;
    }

private:
    TrieNode *root;
};

图论

通用深度优先搜索

    bool dfs(const vector<set<int>>& adjacent,vector<bool>& marked,int curVertex,int targetVertex){
        if(curVertex == targetVertex) return true;//something judgement here
        marked[curVertex] = true;//marked current vertex
        for(auto& adjVtx : adjacent[curVertex]){
            if(!marked[adjVtx] && dfs(adjacent,marked,adjVtx,targetVertex))//search adjacent
                return true;
        }
        return false;//nothing found
    }

有向图存在环路的判断

你这个学期必须选修 numCourse 门课程,记为 0numCourse-1 。在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]

给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?

示例 :

输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。

邻接表表示有向图,利用标志数组求解有向图是否存在环路。其中flag = 0表示未被访问;flag = 1表示已被当前顶点启动的DFS访问过;flag = -1表示已被其他节点启动的DFS访问过。

    bool canFinish(int numCourses, vector<vector<int>> &prerequisites)
    {
        vector<vector<int>> adjacent(numCourses);//邻接表
        vector<int> flag(numCourses);//访问标志位
        //build adjacent(构建邻接表)
        for (int i = 0; i < prerequisites.size(); i++) {
            adjacent[prerequisites[i][0]].push_back(prerequisites[i][1]);
        }
        //dfs for each vertex(对当前顶点启动DFS)
        for (int i = 0; i < numCourses; i++) {
            if (!dfs(adjacent, flag, i)) return false;
        }
        return true;
    }
    bool dfs(const vector<vector<int>> &adjacent, vector<int> &flag, int vertex)
    {
        if (flag[vertex] == 1) return false;//已访问过当前节点,判断有环
        if (flag[vertex] == -1) return true;//已被其他节点访问过,无需继续
        flag[vertex] = 1;//当前节点进入
        for (auto &v : adjacent[vertex]) {
            if (!dfs(adjacent, flag, v)) {
                return false;//判断有环
            }
        }
        flag[vertex] = -1;//当前节点离开
        return true;
    }

有向图拓扑排序问题

  • 问题描述

    给定一个包含 n 个节点的有向图 G,我们给出它的节点编号的一种排列,如果满足:

    对于图 G 中的任意一条有向边 (u, v)u在排列中都出现在 v 的前面。

    那么称该排列是图 G 的「拓扑排序」。

  • 顶点状态

    对于图中的任意一个节点,它在搜索的过程中有三种状态,即:

    「未搜索」:我们还没有搜索到这个节点,用0标记;

    「搜索中」:我们搜索过这个节点,但还没有回溯到该节点,即该节点还没有入栈,还有相邻的节点没有搜索完成,用1标记;

    「已完成」:我们搜索过并且回溯过这个节点,即该节点已经入栈,并且所有该节点的相邻节点都出现在栈的更底部的位置,满足拓扑排序的要求,用-1标记

  • 深度优先搜索

        vector<int> findOrder(int numCourses, vector<vector<int>> &prerequisites)
        {
            vector<vector<int>> adjacent(numCourses);
            vector<int> flag(numCourses, 0);
            vector<int> ans;
            //build adjacent
            for (int i = 0; i < prerequisites.size(); i++)
                adjacent[prerequisites[i][0]].push_back(prerequisites[i][1]);
            for (int i = 0; i < numCourses; i++)
                if(!dfs(adjacent,flag,i,ans)) return {};
            return ans;
        }
    
        bool dfs(const vector<vector<int>> &adjacent, vector<int> &flag,
                 int curVertex, vector<int> &order)
        {
            if (flag[curVertex] == 1) return false; //搜索中第二次访问
            if (flag[curVertex] == -1) return true; //已访问节点
            flag[curVertex] = 1;//标记
            for (auto &adjVtx : adjacent[curVertex])
                if (!dfs(adjacent, flag, adjVtx, order))
                    return false;
            flag[curVertex] = -1;//回溯改标记
            order.push_back(curVertex);//入栈
            return true;
        }
    
    • 广度优先搜索
        vector<int> findOrder(int numCourses, vector<vector<int>> &prerequisites)
        {
            vector<int> ans;
            vector<int> indegree(numCourses, 0);//顶点入度
            queue<int> q;//中间栈
            vector<vector<int>> adjacent(numCourses);//邻接矩阵
            for (auto &edge : prerequisites) {
                adjacent[edge[1]].push_back(edge[0]);//逆序构建邻接表
                indegree[edge[0]]++;//统计顶点入度
            }
            for (int i = 0; i < numCourses; i++) {
                if (indegree[i] == 0) {
                    q.push(i);
                    ans.push_back(i);
                }
            }
    
            while (!q.empty()) {
                int curVertex = q.front();
                q.pop();
                for (auto &adjVtx : adjacent[curVertex]) {
                    indegree[adjVtx]--;//将当前顶点的邻接顶点入度 -1 
                    if (indegree[adjVtx] == 0) {
                        q.push(adjVtx);//如果入度为0,入栈,重复此过程
                        ans.push_back(adjVtx);
                    }
                }
            }
            if (ans.size() != numCourses) return {};
            return ans;
        }
    

无向图的深拷贝(克隆)

  • 问题描述

    给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。

    图中的每个节点都包含它的值 valint) 和其邻居的列表(list[Node])。

    class Node {
        public int val;
        public List<Node> neighbors;
    }
    
  • 拷贝过程

    利用hashmap记录原节点对应的新顶点,并通过查找hashmap的方法判断是否重复构建;

        if (hash.find(n) != hash.end())//表中已记录,节点已经构建
        	root->neighbors.push_back(hash[n]);
        else
        	root->neighbors.push_back(cloneGraph(n));//表中无记录,需要构建节点
    
  • 递归实现

    class Solution
    {
    public:
        Node *cloneGraph(Node *node)
        {
            if (node == nullptr) return nullptr;
            int val = node->val;
            Node *root = new Node(val);
            hash[node] = root;
            for (auto &n : node->neighbors) {
                if (hash.find(n) != hash.end())
                    root->neighbors.push_back(hash[n]);
                else
                    root->neighbors.push_back(cloneGraph(n));
            }
            return root;
        }
    private:
        unordered_map<Node *, Node *> hash;
    };
    
  • 迭代实现

     class Solution
    {
    public:
    	Node *cloneGraph(Node *node)
        {
            if (node == nullptr) return nullptr;
            queue<Node *> q({node});
            hash[node] = new Node(node->val);
            while (!q.empty()) {
                Node *tmp = q.front();
                q.pop();
                for (auto &n : tmp->neighbors) {
                    if (hash.find(n) == hash.end()) {
                        hash[n] = new Node(n->val);
                        q.push(n);//初次访问节点入队
                    }
                    hash[tmp]->neighbors.push_back(hash[n]);//链接节点
                }
            }
            return hash[node];
        }
    private:
        unordered_map<Node *, Node *> hash;
    

并查集查找构成环路的冗余边

  • 问题描述

    在本问题中, 树指的是一个连通且无环的无向图。

    输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, …, N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。

    结果图是一个以组成的二维数组。每一个的元素是一对[u, v] ,满足 u < v,表示连接顶点uv无向图的边。

    返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v

    示例 2:

    输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
    输出: [1,4]
    解释: 给定的无向图为:
    5 - 1 - 2
        |   |
        4 - 3
    
  • 并查集代码实现

    	int *parent = nullptr;
    	//查找parent
        int find_root(int x)
        {
            while (parent[x] != x) {
                parent[x] = parent[parent[x]];
                x = parent[x];
            }
            return x;
        }
    	//合并parent
        bool union_root(int x, int y)
        {
            int root_x = find_root(x);
            int root_y = find_root(y);
            if (root_x == root_y) return false;
            parent[root_x] = root_y;
            return true;
        }
    
        vector<int> findRedundantConnection(vector<vector<int>> &edges)
        {
            parent = new int[1001];
            for (int i = 0; i < 1001; i++) parent[i] = i;
            
            for (auto e : edges) 
                if (!union_root(e[0], e[1])) return e;
            
            delete[] parent;
            return {};
        }
    

二分图(染色问题)

  • 886. 可能的二分法

    构建无向图,之后用dfs遍历图并染色,过程中判断是否颜色相同。

        void dfs(const vector<vector<int>>& adjacent,vector<bool>& flag,vector<bool>& color,int curVertex)
        {
            flag[curVertex] = true;
            for(auto& v : adjacent[curVertex]){
                if(!flag[v]){
                    color[v] = !color[curVertex];
                    dfs(adjacent,flag,color,v);
                }else if(color[v] == color[curVertex]) 
                    isTwoColorable = false;
            }
        }
    

最低成本联通(最小生成树)

  • 问题描述

    想象一下你是个城市基建规划者,地图上有 N 座城市,它们按以 1N 的次序编号。

    给你一些可连接的选项 conections,其中每个选项 conections[i] = [city1, city2, cost] 表示将城市 city1 和城市 city2 连接所要的成本。(连接是双向的,也就是说城市 city1 和城市 city2 相连也同样意味着城市 city2 和城市 city1 相连)。

    返回使得每对城市间都存在将它们连接在一起的连通路径(可能长度为 1 的)最小成本。该最小成本应该是所用全部连接代价的综合。如果根据已知条件无法完成该项任务,则请你返回 -1

最短路径

  • 问题描述

    单点最短路径。给定一幅加权有向图和一个起点s,回答“从s 到给定的目的顶点v 是否存在一条有向路径?如果有,找出最短(总权重最小)的那条路径”。

其他问题

二进制中1的个数

0101(5)
&0100(4)
=0100(4)
&0011(3)
=0000(0)
int countOnes(int n){
	int cnt = 0;
	while(n != 0){
		n &= (n-1);
		cnt++;
	}
	return cnt;
}
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐