算法学习 动态规划解题框架

动态规划

原文地址

  • 动态规划的一般形式就是求最值
    动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离等等。
  • 动态规划核心
    既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值呗。

动态规划三要素

  • 重叠子问题:在穷举的过程中往往会有许多重复子问题,导致效率极其低下。所以需要备忘录和DP table来优化穷举结构。
  • 最优子结构:一定要具备最优子结构(子问题相互独立),才可以通过子问题的最值得到原问题的最值。比如考试总分要考的最高,那就需要你语文、数学和英语每一门的单科考的最高。但是你语文考的怎么样和你数学靠的怎么样没有关系,“每一门科目考到最”这些子问题是独立的问题,符合最优子结构;
  • 状态转移方程(最困难):流程化确定状态转移方程
    明确[状态]->定义dp数组/函数的含义->明确[选择]->明确base case

斐波那数列

  • 首先我们分析暴力递归
int fib(int N) {
    if (N == 1 || N == 2) return 1;
    return fib(N - 1) + fib(N - 2);
}

暴力递归十分低效,通过画出递归树来分析算法复杂度:
在这里插入图片描述
因为树中有太多的重复子节点而导致大量重复计算。复杂度=递归树中节点总数x每个节点的时间复杂度=O(2^n)xO(1);
要解决重复子问题那么自然想到将所有未出现的节点放入一个数组或者哈希表中去,一旦再次出现这个节点直接取表中数据,如果出现新的节点就放入表中。

  • 带备忘录的递归解法
int Fib(int n)
{
	if(n<1) return 0;
	vector<int>meno(n+1,0);
	
	return FibCore(meno,n);
}

int FibCore(vector<int> meno,int n)
{
	if(n==1||n==2)return 1;
	if(meno[n]!=0)return meno[n];
	else{
		meno[n]=FibCore(n-1)+FibCore(n-2);
	}
	return meno[n];
}

在这里插入图片描述
实际上,带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。
在这里插入图片描述
至此,带备忘录的递归解法的效率已经和迭代的动态规划解法一样了。实际上,这种解法和迭代的动态规划已经差不多了,只不过这种方法叫做「自顶向下」,动态规划叫做「自底向上」。

啥叫「自顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说 f(20),向下逐渐分解规模,直到 f(1) 和 f(2) 触底,然后逐层返回答案,这就叫「自顶向下」。

啥叫「自底向上」?反过来,我们直接从最底下,最简单,问题规模最小的 f(1) 和 f(2) 开始往上推,直到推到我们想要的答案 f(20),这就是动态规划的思路,这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算。

  • dp 数组的迭代解法
int Fib(int n)
{
	vector<int>dp(n+1,0);
	dp[1]=1;
	dp[2]=1;
	for(int i=3;i<=n;i++)
		dp[i]=dp[i-1]+dp[i-2];
	return dp[n];
}

在这里插入图片描述
状态转移方程:
在这里插入图片描述
f(n)这个状态是如何由其他状态得到就是状态转移方程要干的事。

凑零钱问题

题目:给你k种面值的硬币,面值分别为 c1, c2 … ck,每种硬币的数量无限,再给一个总金额amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回-1。算法的函数签名如下:

// coins 中是可选硬币面值,amount 是目标金额
int coinChange(int[] coins, int amount);

如何列出正确的状态转移方程?

  1. 确定状态:也就是确定原问题和子问题中变化的量,在改变的是金钱的总额amount
  2. 确定dp定义:dp[n] 金额为n时需要的硬币个数
  3. 确定[选择]并择优:也就是对于每个状态,可以做出什么选择改变当前状态。min(1+dp[n-coin]|coin∈coins)
  4. 确定base case:dp[0]=0;
#include "stdafx.h"
#include <vector>
#include <iostream>

int coinChange(std::vector<int>&coins, int amount)
{
	if (coins.size() <= 0 || amount <= 0)//异常输入
		return -1;
	//std::vector<int>dp(amount + 1, 0);//dp table
	std::vector<int>dp(amount + 1, amount + 1);//1
	dp[0] = 0;

	for (int i = 1; i <= amount; i++)
	{
		int count = INT_MAX;
		//for(auto it=coins.begin();it!=coins.end();it++)
		for (int coin:coins)//2
		{
			if ((i - coin) < 0)continue;
			else
			{
				if (dp[i - coin] + 1 < count)count = dp[i - coin] + 1;
			}
		}
		dp[i] = count;
	}
	return dp[amount];
	
}


void Test(char* testName, std::vector<int>&coins, int amount, int exptection)
{
	if (testName != nullptr)
		std::cout << testName << ":" ;
	int result = coinChange(coins, amount);
	if (result == exptection)
		std::cout << "passed" << std::endl;
	else
		std::cout << "failed" << std::endl;
}

void test1()
{
	char*testName = "test1";
	std::vector<int>coins = { 1,2,5 };
	int amount = 11;
	int exptection = 3;
	Test(testName, coins, amount, exptection);
}

int main()
{
	test1();
    return 0;
}

最优子结构详解

可以从子问题的最优结果推出更大规模问题的最优结果(子问题之间相互独立)
最优子结构的性质作为动态规划问题的必要条件一定是让你求最值的。

dp数组的遍历方向

  1. 遍历的过程中,所需的状态是已经计算出来的。
  2. 遍历的终点必须是存储结果的那个位置。

练习题:

1.剑指 Offer 14- I. 剪绳子

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),
每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]*k[1]*...*k[m-1] 可能的最大乘积是多少?
例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

1.状态(原问题和子问题中一直在变化的量):绳子的剩余长度
2.dp函数定义:dp(n)长度为n的绳子任意剪成m段的乘积
3.选择(对于当前状态,做什么改变改变状态):dp(n)=max(2xdp[n-2],3xdp[n-3]);
4.base case:dp[0]=0;dp[1]=1;dp[2]=2;dp[3]=3(当输入的n直接为2或者3的时候另算)

class Solution {
public:
    int cuttingRope(int n) {
        if(n==0||n==1)
            return -1;
        if(n==2)
            return 1;
        if(n==3)
            return 2;
        vector<int>dp(n+1,0);
        dp[0]=0;
        dp[1]=1;
        dp[2]=2;
        dp[3]=3;
        for(int i=4;i<=n;i++)
            dp[i]=max(2*dp[i-2],3*dp[i-3]);
        return dp[n];
    }
};

2.不同的二叉搜索树

给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?

示例:

输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

状态:节点数量
dp函数:节点为n时的二叉搜索树总数
选择:f(n)=Σf(i)xf(n-i-1)(0<=i<n)
base case:f0=1;f1=1;(零个节点和一个节点都看成1)

class Solution {
public:
    int numTrees(int n) {
        if(n<=0)
            return -1;
        vector<int>dp;
        dp.push_back(1);
        dp.push_back(1);
        for(int i=2;i<=n;i++)
        {
            int num=0;
            for(int j=0;j<i;j++)
            {
                num+=(dp[j]*dp[i-j-1]);
            }
            dp.push_back(num);
        }
        return dp[n];
    }
};

3.通配符匹配

给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。

'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。
示例 1:

输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:

输入:
s = "aa"
p = "*"
输出: true
解释: '*' 可以匹配任意字符串。
示例 3:

输入:
s = "cb"
p = "?a"
输出: false
解释: '?' 可以匹配 'c', 但第二个 'a' 无法匹配 'b'。
示例 4:

输入:
s = "adceb"
p = "*a*b"
输出: true
解释: 第一个 '*' 可以匹配空字符串, 第二个 '*' 可以匹配字符串 "dce".
示例 5:

输入:
s = "acdcb"
p = "a*c?b"
输出: false
  • 状态转移方程
  1. 状态 s和p的下标
  2. dp[i][j]:s的前i个字符和p的前j个字符是否匹配
  3. 状态转移:
    (1)如果s[i-1]==p[j-1]或p[j-1] ==’?’:dp[i][j]=dp[i-1][j-1]
    (2)如果p[j-1] ==’’:dp[i][j]=dp[i-1][j]||dp[i][j-1]
    4.base case:
    (1)dp[0][0]=true:都为空
    (2)dp[i][0]=false:p字符用完了
    (3)dp[0][j]=如果p前j个全都是
    true否则false
class Solution {
public:
    bool isMatch(string s, string p) 
    {
        int s_size=s.size();
        int p_size=p.size();
        vector<vector<bool>>dp(s_size+1,vector<bool>(p_size+1,false));
        dp[0][0]=true;
        for(int i=1;i<=p_size;i++)
        {
            if(p[i-1]!='*')
                break;
            else
                dp[0][i]=true;
        }

        for(int i=1;i<=s_size;i++)
            {
                for(int j=1;j<=p_size;j++)
                {
                    if(s[i-1]==p[j-1]||p[j-1]=='?')
                        dp[i][j]=dp[i-1][j-1];
                    else if(p[j-1]=='*')
                        dp[i][j]=dp[i-1][j]||dp[i][j-1];

                }
            }
        return dp[s_size][p_size];    
    }

};

4.不同路径 二

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

在这里插入图片描述

网格中的障碍物和空位置分别用 1 和 0 来表示。
说明:m 和 n 的值均不超过 100。
示例 1:
输入:
[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
  • 第一反应是用回溯法解决,但是超出时间限制
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        if(obstacleGrid.size()<=0)
            return 0;
        int row=obstacleGrid.size();
        int col=obstacleGrid[0].size();
        //vector<vector<bool>>havevisit(row,vector<bool>(col,false));
        //vector<pair<int,int>>track;
        //track.public(make_pair(0,0));
        if(obstacleGrid[0][0]==1||obstacleGrid[row-1][col-1]==1)
            return 0;
        backtrack(obstacleGrid,row,col,0,0);
        return pathcount;
    }
    void backtrack(vector<vector<int>>&obstacleGrid,int row,int col,int currow,int curcol)
    {
        if(currow==row-1&&curcol==col-1&&obstacleGrid[currow][curcol]==0)
            {
            pathcount++;
            return;
            }
        for(int i=0;i<2;i++)
        {
            currow+=i;
            curcol+=(1-i);
            if(!(curcol>=col||currow>=row||obstacleGrid[currow][curcol]==1))
                backtrack(obstacleGrid,row,col,currow,curcol);
            currow-=i;
            curcol-=(1-i);
        }
    }
private:
    int pathcount=0;
};
  • 既然超出时间限制,那么就考虑动态规划剪枝叶
    状态转移方程
    1. 状态,改变的是坐标
    2. dp函数定义,dp[i][j]表示走到当前位置有多少种走法
    3. 状态转移,dp[i][j]=dp[i-1][j]+dp[i][j-1]
    4. base case,dp[0][0]=1,有障碍的地方设置为0
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        if(obstacleGrid.size()<=0)
            return 0;
        int row=obstacleGrid.size();
        int col=obstacleGrid[0].size();
        if(obstacleGrid[0][0]==1||obstacleGrid[row-1][col-1]==1)
            return 0;
        vector<vector<int>>dp(row+1,vector<int>(col+1,0));
        for(int currow=1;currow<=row;currow++)
            {
                for(int curcol=1;curcol<=col;curcol++)
                {
                    if(obstacleGrid[currow-1][curcol-1]==1)
                        dp[currow][curcol]=0;
                    else if(currow==1&&curcol==1)
                        dp[currow][curcol]=1;
                    else
                        dp[currow][curcol]=dp[currow-1][curcol]+dp[currow][curcol-1];
                }
            }

        return dp[row][col];
    }
};

5.恢复空格(字典树&动态规划)

哦,不!你不小心把一个长篇文章中的空格、标点都删掉了,并且大写也弄成了小写。像句子"I reset the computer. It still didn’t boot!"已经变成了"iresetthecomputeritstilldidntboot"。在处理标点符号和大小写之前,你得先把它断成词语。当然了,你有一本厚厚的词典dictionary,不过,有些词没在词典里。假设文章用sentence表示,设计一个算法,把文章断开,要求未识别的字符最少,返回未识别的字符数。

注意:本题相对原题稍作改动,只需返回未识别的字符数

 

示例:

输入:
dictionary = ["looked","just","like","her","brother"]
sentence = "jesslookedjustliketimherbrother"
输出: 7
解释: 断句后为"jess looked just like tim her brother",共7个未识别字符。
提示:

0 <= len(sentence) <= 1000
dictionary中总字符数不超过 150000。
你可以认为dictionary和sentence中只包含小写字母。
  • 字典树

      Trie树,又称字典树、单词查找树、前缀树,是一种哈希树的变种,应用于字符串的统计与排序,经常被搜索引擎系统用于文本词频统计。
      优点是查询快,利用字串的公共前缀来节省存储空间,最大限度的减少无谓的字串比较。
      对于长度为m的键值,最坏情况下只需花费O(m)的时间;而BST需要O(mlogn)的时间。
    
class Trie{
public:
	Trie* next[26]="nullptr";//当前节点的子节点,初始化位nullptr
	bool isEnd;//是否是结束节点

	Tire(){
		isEnd=false;
	}
	
	void Insert(string s){
		Trie* curPos=this;
		for(int i=s.length()-1;i>=0;i--)//从尾到头遍历
		{
			int t=s[i]-'a';
			if(curPos->next[t]==nullptr)//如果首次出现则创建子节点
				curPos->next[t]=new Trie();
			curPos=curPos->next[t];
		}
		curPos->isEnd=true;
	}
}
  • 动态规划
    状态:字符下标
    dp函数:当前下标前的所有未定义字符总数
    状态转移:dp[i]=dp[i-1]+1,如果前面没有匹配字符
    min(dp[i],dp[j-1]),如果匹配到了j到i的字符
    base case:dp[0]=0;
class Solution{
public:int respace(vector<string>&dictionary,string sentence){
	int n=sentence.length()-1,inf=0x3f3f3f3f;
	
	Tire* root=new Trie();
	for(auto &str:dictionary)//将字典里的值插入字典树
	{
		root->insert(str);
	}
	vector<int>dp(n+1,ing);//dp函数
	dp[0]=0;
	for(int i=1;i<=n;i++)//遍历
	{
		dp[i]=dp[i-1]+1;//更新dp
		Trie *curPos=root;
		for(int j=i;j>0;j--)//在字典树中匹配单词
		{
			int t=sentence[i]-'a';
			if(curPos->next[t]==nullptr)
				break;
			else if(curPos->isEnd)
				dp[i]=min(dp[i],dp[j-1]);//选择状态转移方式
			if(dp[i]==0)
				break;
			curPos=curPos->next[t];
		}
	}
	return dp[n];
	}
}

6.地下城游戏

一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。

骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。

有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快到达公主,骑士决定每次只向右或向下移动一步。
编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。

例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7。

-2 (K)	-3	3
-5	-10	1
10	30	-5 (P)
 
说明:

骑士的健康点数没有上限。

任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
  • 自己的思路:(无法通过示例)
    1.状态:当前所处的位置(i,j)
    2.dp[i][j]:first到达当前位置的累计和,second到达当前路径上的所需的最小初始值
    3.dp[][]=略
    4.base case dp[0][0]=dangeon[0][0]
class Solution {
public:
	int calculateMinimumHP(vector<vector<int>>& dungeon) {
		if (dungeon.size() == 0)
			return 0;
		int row = dungeon.size();
		int col = dungeon[0].size();
		vector<vector<pair<int, int>>>dp(row, vector<pair<int, int>>(col, make_pair(0, 0)));//当前血量,过程中最小值
		dp[0][0].first = dungeon[0][0];
		dp[0][0].second = dungeon[0][0];
		for (int i = 0; i < row; i++)
		{
			for (int j = 0; j < col; j++)
			{
				if (i == 0 && j == 0)continue;
				if (i == 0) {
					int minval = dp[i][j-1].second;
					dp[i][j].first = dungeon[i][j] + dp[i][j - 1].first;
					dp[i][j].second = min(dp[i][j].first, minval);
				}
				else if (j == 0) {
					int minval = dp[i-1][j].second;
					dp[i][j].first = dungeon[i][j] + dp[i - 1][j].first;
					dp[i][j].second = min(dp[i][j].first, minval);
				}
				else {
					int curleft = dp[i][j - 1].first + dungeon[i][j];
					int curup = dp[i - 1][j].first + dungeon[i][j];
					int curleftmin = min(dp[i][j - 1].second, curleft);
					int curupmin = min(dp[i - 1][j].second, curup);
					dp[i][j].first = curleftmin > curupmin ? curleft : curup;
					dp[i][j].second = curleftmin > curupmin ? curleftmin : curupmin;
				}
			}
		}
		return dp[row - 1][col - 1].second > 0 ? 1 : -dp[row - 1][col - 1].second + 1;
	}
};

  • 题解思路(反向动态规划)
    1.状态:当前所处的位置(i,j)
    2.dp函数的意义:当前位置到终点所需的最小初始值(不包括当前位置的值)
    3.dp[i][j]=(min(dp[i+1][j],dp[i][j+1])-dangous[i][j],1)//minn-dungeon(i,j):当前需要的最小初始值-这个位置已有的值,初始值必须大于等于1
    4base case dp[n-1][m]=dp[n][m-1]=1
class Solution {
public:
	int calculateMinimumHP(vector<vector<int>>& dungeon) {
		if (dungeon.size() == 0)
			return 0;
		int row = dungeon.size();
		int col = dungeon[0].size();
        vector<vector<int>>dp(row+1,vector<int>(col+1,INT_MAX));
        dp[row][col-1]=dp[row-1][col]=1;
        for(int i=row-1;i>=0;i--)
        {
            for(int j=col-1;j>=0;j--)
            {
                int mindp=min(dp[i+1][j],dp[i][j+1]);
                dp[i][j]=max(mindp-dungeon[i][j],1);
            }
        }
        return dp[0][0];
	}
};

7.三角形最小路径和

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。

例如,给定三角形:

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

说明:

如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。
class Solution {
public:
	int minimumTotal(vector<vector<int>>& triangle) {
		int n = triangle.size();
		vector<int> f(n);//高和宽是一样的
		f[0] = triangle[0][0];
		for (int i = 1; i < n; ++i) {
			f[i] = f[i - 1] + triangle[i][i];//先处理最后一位
			for (int j = i - 1; j > 0; --j) {
				f[j] = min(f[j - 1], f[j]) + triangle[i][j];
			}
			f[0] += triangle[i][0];//最后处理第一位第一位
		}
		return *min_element(f.begin(), f.end());
	}
};

8.交错字符串

给定三个字符串 s1, s2, s3, 验证 s3 是否是由 s1 和 s2 交错组成的。

示例 1:

输入: s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac”
输出: true
示例 2:

输入: s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbbaccc”
输出: false
在这里插入图片描述

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int row=s1.length();
        int col=s2.length();
        int sumlength=s3.length();
        if((row+col)!=sumlength)
            return false;
        vector<vector<bool>>dp(row+1,vector<bool>(col+1,false));
        dp[0][0]=true;
        for(int i=0;i<=row;i++)
        {
            for(int j=0;j<=col;j++)
            {
                if(i==0&&j==0)continue;
                if(i==0&&j!=0)
                {
                    dp[i][j]=(dp[i][j-1]&&(s2[j-1]==s3[i+j-1]));
                    continue;
                }
                if(i!=0&&j==0)
                {
                    dp[i][j]=(dp[i-1][j]&&(s1[i-1]==s3[i+j-1]));
                    continue;
                }
                dp[i][j]=(dp[i-1][j]&&(s1[i-1]==s3[i+j-1]))||(dp[i][j-1]&&(s2[j-1]==s3[i+j-1]));
            }
        }
        return dp[row][col];
    }
};

9.最长递增子序列

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

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:

可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int length=nums.size();
        if(length==0)return 0;
        vector<int>dp(length,1);
        dp[0]=1;

        for(int i=1;i<length;i++)
        {
            for(int j=0;j<i;j++)
            {
                if(nums[j]<nums[i])
                {
                    dp[i]=max(dp[i],dp[j]+1);//不要用额外的变量,直接用dp[i]
                }
            }
        }
        int ret=INT_MIN;
        for(auto num:dp)
        {
            ret=max(ret,num);
        }
        return ret;
    }
};

10.判断子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例 1:
s = "abc", t = "ahbgdc"

返回 true.

示例 2:
s = "axc", t = "ahbgdc"

返回 false.

后续挑战 :

如果有大量输入的 S,称作S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

1.状态dp:dp[i][j]:t中第i个字符后面出现第一个字符j的位置
2.转移:dp[i][j]=if(ti=j)=i
if(ti!=j)=dp[i+1][j]
3.基准值:dp[m][…]=m

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int slen=s.size();
        int tlen=t.size();
        vector<vector<int>>dp(tlen+1,vector<int>(26,0));//维护一个tlenx26的dp表
        
        for(int i=0;i<26;i++)dp[tlen][i]=tlen;//dp表最后一行全部为tlen,基准值

        for(int i=tlen-1;i>=0;i--)
        {
        //********j写成i
            for(int j=0;j<26;j++)
            {
                if(t[i]==j+'a')dp[i][j]=i;//如果t当前位置字母和j位置匹配,那么记录当前位置
                else dp[i][j]=dp[i+1][j];//如果不匹配,那么用后一位置的替代
            }
        }
        int add=0;
        for(int i=0;i<slen;i++)//查询s
        {
            if(dp[add][s[i]-'a']==tlen)//如果=tlen说明t中没有匹配字符返回false
                return false;
            add=dp[add][s[i]-'a']+1;   //如果有匹配下一个字符
        }
        return true;
    }
};

11.编辑距离

详细解释
状态:dp[i][j]代表最小编辑距离
转移:如果s1[i]==s2[j] 那么dp[i][j]=dp[i-1][j-1]
如果s1[i]!=s2[j]就有三种情况
1.删除:dp[i][j]=dp[i-1][j]+1
2.插入:dp[i][j]=dp[i][j-1]+1
3.替换:dp[i][j]=dp[i-1][j-1]+1
base case:当i或者j=0的时候,dp=剩余字符串的个数

class Solution {
public:
    int min_tri(int a,int b,int c)
    {
        return min(a,min(b,c));
    }
    int minDistance(string word1, string word2) {
        
        int w1_len=word1.size(),w2_len=word2.size();
        vector<vector<int>>dp(w1_len+1,vector<int>(w2_len+1,0));
        for(int i=0;i<=w1_len;i++)dp[i][0]=i;
        for(int i=0;i<=w2_len;i++)dp[0][i]=i;

        for(int row=1;row<=w1_len;row++)
        {
            for(int col=1;col<=w2_len;col++)
            {
            	//=写成了==
                if(word1[row-1]==word2[col-1])dp[row][col]=dp[row-1][col-1];
                else{
                    dp[row][col]=min_tri(dp[row-1][col-1],dp[row-1][col],dp[row][col-1])+1;
                }
            }
        }
        return dp[w1_len][w2_len];
    }
};

12.非递减序列,两位选手,a先手删除一个字符的第一个数,谁删除最后g

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值