数据结构与算法:二维动态规划
前言
当递归函数有两个可变参数时,改成的动态规划就是二维的。
一、从递归到二维动态规划
1.最小路径和
class Solution {
public:
vector<int>move={0,1,0};
static bool cmp(vector<int>&a,vector<int>&b)
{
return a[2]>b[2];
}
int minPathSum(vector<vector<int>>& grid) {
int n=grid.size();
int m=grid[0].size();
//return dijkstra(n,m,grid);
//return recursion(0,0,n,m,grid);
//vector<vector<int>>dp(n,vector<int>(m,-1));
//return memorized_search(0,0,n,m,grid,dp);
//return dynamic_programming(n,m,grid);
return optimized_dp(n,m,grid);
}
int dijkstra(int n,int m,vector<vector<int>>&grid)
{
vector<vector<int>>distance(n,vector<int>(m,INT_MAX));
distance[0][0]=grid[0][0];
vector<vector<bool>>visited(n,vector<bool>(m,false));
priority_queue<vector<int>,vector<vector<int>>,decltype(&cmp)>heap(cmp);
heap.push({0,0,grid[0][0]});
while(!heap.empty())
{
int x=heap.top()[0];
int y=heap.top()[1];
heap.pop();
if(!visited[x][y])
{
if(x==n-1&&y==m-1)
{
return distance[x][y];
}
visited[x][y]=true;
for(int i=0;i<2;i++)
{
int nx=x+move[i];
int ny=y+move[i+1];
if(nx>=0&&nx<n&&ny>=0&&ny<m&&!visited[nx][ny])
{
if(grid[nx][ny]+distance[x][y]<distance[nx][ny])
{
distance[nx][ny]=grid[nx][ny]+distance[x][y];
heap.push({nx,ny,distance[nx][ny]});
}
}
}
}
}
return -1;
}
//递归 -> 超时
int recursion(int x,int y,int n,int m,vector<vector<int>>&grid)
{
if(x==n-1&&y==m-1)
{
return grid[x][y];
}
if(x<0||x>n-1||y<0||y>m-1)
{
return INT_MAX;
}
int ans=grid[x][y]+min(recursion(x,y+1,n,m,grid),recursion(x+1,y,n,m,grid));
return ans;
}
//记忆化搜索 -> 超时
int memorized_search(int x,int y,int n,int m,vector<vector<int>>&grid,vector<vector<int>>&dp)
{
if(x==n-1&&y==m-1)
{
return grid[x][y];
}
if(x<0||x>n-1||y<0||y>m-1)
{
return INT_MAX;
}
if(dp[x][y]!=-1)
{
return dp[x][y];
}
int ans=grid[x][y]+min(recursion(x,y+1,n,m,grid),recursion(x+1,y,n,m,grid));
dp[x][y]=ans;
return ans;
}
//动态规划
int dynamic_programming(int n,int m,vector<vector<int>>&grid)
{
vector<vector<int>>dp(n,vector<int>(m));
dp[n-1][m-1]=grid[n-1][m-1];
for(int i=n-1;i>=0;i--)
{
for(int j=m-1;j>=0;j--)
{
if(i==n-1&&j==m-1)
{
continue;
}
dp[i][j]=grid[i][j]+min(i+1<=n-1?dp[i+1][j]:INT_MAX,j+1<=m-1?dp[i][j+1]:INT_MAX);
}
}
return dp[0][0];
}
//空间压缩
int optimized_dp(int n,int m,vector<vector<int>>&grid)
{
vector<int>dp(m);
//初始化
dp[m-1]=grid[n-1][m-1];
for(int j=m-2;j>=0;j--)
{
dp[j]=dp[j+1]+grid[n-1][j];
}
for(int i=n-2;i>=0;i--)
{
dp[m-1]=grid[i][m-1]+dp[m-1];
for(int j=m-2;j>=0;j--)
{
dp[j]=grid[i][j]+min(dp[j],dp[j+1]);
}
}
return dp[0];
}
};
其实这个题用dijkstra也能做。()
观察可知,递归函数肯定有两个可变参数,一个是x坐标一个是y坐标。那么之后就是到了终点就返回终点格子的大小,否则就是当前格子的大小加上下边和右边的最小值。又因为要比较最小值,所以当越界的时候要返回无穷大。
之后记忆化搜索就是挂个dp表即可。
再就是改动态规划,在二维动态规划中,由于有两个可变参数,所以dp表呈现的形式是一个二维网格。之后分析严格位置依赖可知,每个格子依赖自己右方和下方的格子,那么在填格子的时候就从下往上,从右往左填。
然后因为只依赖自己下方和右方的格子,那么就可以只用一个一维数组,只表示当前一行。之后从下往上滚动更新即可。
2.单词搜索
这个题想说明的就是带路径的递归不适合改动态规划。
class Solution {
public:
vector<int>move={-1,0,1,0,-1};
bool exist(vector<vector<char>>& board, string word) {
for(int i=0;i<board.size();i++)
{
for(int j=0;j<board[0].size();j++)
{
if(recursion(i,j,0,word,board))
{
return true;
}
}
}
return false;
}
bool recursion(int x,int y,int i,string &word,vector<vector<char>>&board)
{
if(i==word.length())
{
return true;
}
if(x<0||x>=board.size()||y<0||y>=board[0].size()||board[x][y]!=word[i])
{
return false;
}
char tmp=board[x][y];//带路径!!!
board[x][y]=0;
bool ans=false;
for(int j=0;j<4;j++)
{
ans|=recursion(x+move[j],y+move[j+1],i+1,word,board);
}
board[x][y]=tmp;//还原现场
return ans;
}
};
因为要求不能走回头路,那么每来到一个格子,都要先把当前格子改成0,之后在回来的时候再还原。那么由此可知,每次递归的时候,可变参数不只两个坐标,还有整个棋盘格,所以没法改成动态规划。
那么整体思路就是遍历棋盘格,即讨论以每个点开头的情况,找到就返回。然后递归函数的可变参数就是两个坐标和当前到的字符串的位置。之后若把字符串遍历到头了就说明找到了,返回true,越界或格子字符对不上就返回false。之后考虑上下左右四个位置,有一个可以就是true,那么就是把四个位置的情况都或起来即可。
3.最长公共子序列
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n=text1.length();
int m=text2.length();
//return recursion(n-1,m-1,text1,text2);
//return optimized_recursion(n,m,text1,text2);
//vector<vector<int>>dp(n+1,vector<int>(m+1,-1));
//return memorized_search(n,m,text1,text2,dp);
//return dynamic_programming(n,m,text1,text2);
return optimized_dp(n,m,text1,text2);
}
//递归 -> 超时
int recursion(int i1,int i2,string &text1,string &text2)
{
//以结尾讨论可能性 -> 返回text1[0...i1]和text2[0...i2]最长公共子序列长度
if(i1<0||i2<0)
{
return 0;
}
int p1=recursion(i1-1,i2-1,text1,text2);//都不要
int p2=recursion(i1-1,i2,text1,text2);//不要i1位置
int p3=recursion(i1,i2-1,text1,text2);//不要i2位置
int p4=text1[i1]==text2[i2]?(p1+1):0;//当前位置一样,都要,去之后讨论,同都不要
return max(max(p1,p2),max(p3,p4));
}
//优化递归 -> 超时
int optimized_recursion(int len1,int len2,string &text1,string &text2)
{
//优化:以长度讨论可能性 -> len1=6 =>text1[0...5] -> 避免边界讨论
if(len1==0||len2==0)//不可能小于0
{
return 0;
}
//优化:单调性 -> f(len1-1,len2-1)<=f(len1-1,len2)或f(len1,len2-1)
int ans;
if(text1[len1-1]==text2[len2-1])//优化:贪心 -> 字符相等就要,直接结算
{
ans=optimized_recursion(len1-1,len2-1,text1,text2)+1;
}
else
{
ans=max(optimized_recursion(len1-1,len2,text1,text2),
optimized_recursion(len1,len2-1,text1,text2));
}
return ans;
}
//记忆化搜索
int memorized_search(int len1,int len2,string &text1,string &text2,vector<vector<int>>&dp)
{
if(len1==0||len2==0)
{
return 0;
}
if(dp[len1][len2]!=-1)
{
return dp[len1][len2];
}
int ans;
if(text1[len1-1]==text2[len2-1])
{
ans=memorized_search(len1-1,len2-1,text1,text2,dp)+1;
}
else
{
ans=max(memorized_search(len1-1,len2,text1,text2,dp),
memorized_search(len1,len2-1,text1,text2,dp));
}
dp[len1][len2]=ans;
return ans;
}
//动态规划
int dynamic_programming(int n,int m,string &text1,string &text2)
{
vector<vector<int>>dp(n+1,vector<int>(m+1,0));
//初始化 -> 可省略
// for(int i=0;i<n;i++)
// {
// dp[i][0]=0;
// }
// for(int j=0;j<m;j++)
// {
// dp[0][j]=0;
// }
//严格位置依赖
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(text1[i-1]==text2[j-1])
{
dp[i][j]=dp[i-1][j-1]+1;
}
else
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[n][m];
}
//空间压缩
int optimized_dp(int n,int m,string &text1,string &text2)
{
//让较短的作列 -> 省空间
int row,col;
if(n>=m)
{
row=n;
col=m;
}
else
{
row=m;
col=n;
string tmp=text1;
text1=text2;
text2=tmp;
}
vector<int>dp(col+1,0);
for(int i=1;i<=row;i++)
{
int leftUp=0,backUp;
for(int j=1;j<=col;j++)
{
backUp=dp[j];//备份当前格 -> 记录下一格的左上
if(text1[i-1]==text2[j-1])
{
dp[j]=leftUp+1;
}
else
{
dp[j]=max(dp[j],dp[j-1]);
}
leftUp=backUp;
}
}
return dp[col];
}
};
对于子序列问题,还是从结尾位置考虑。
那么递归尝试就是越界了就返回0,否则考虑四种情况,第一种为两个位置都不要,第二三种分别是只要其中一个,第四种就是若当前字符一样就都要,那么结果就是都不要的情况加一长度,最后返回四种情况的最大值即可。
再分析一下可以发现,还可以优化成从长度考虑,这样可以避免边界的讨论。再进一步观察,可以发现题目具有单调性,即两个都不要的情况得出的子序列长度,一定小于等于要其中一个的情况,那么就可以不讨论都不要的情况。之后,再利用贪心优化一下,就是为了让结果尽可能长,那么当两个位置字符一样时就要,然后直接结算。否则再讨论只要一个的情况。
记忆化搜索和严格位置依赖的动态规划直接照着改就行。
对于空间压缩方法,为了进一步节省空间,可以考虑让较短的字符串作dp表长度。再注意记一下左上位置即可。
4.最长回文子序列
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n=s.length();
//return recursion(0,n-1,s);
//vector<vector<int>>dp(n,vector<int>(n,-1));
//return memorized_search(0,n-1,s,dp);
//return dynamic_programming(n,s);
return optimized_dp(n,s);
}
//递归 -> 超时
int recursion(int l,int r,string &s)
{
if(l==r)
{
return 1;
}
if(l+1==r)
{
return s[l]==s[r]?2:1;//单独一个长度为1
}
if(s[l]==s[r])
{
return 2+recursion(l+1,r-1,s);
}
else
{
return max(recursion(l+1,r,s),recursion(l,r-1,s));
}
}
//记忆化搜索
int memorized_search(int l,int r,string &s,vector<vector<int>>&dp)
{
if(l==r)
{
return 1;
}
if(l+1==r)
{
return s[l]==s[r]?2:1;
}
if(dp[l][r]!=-1)
{
return dp[l][r];
}
int ans;
if(s[l]==s[r])
{
ans=2+memorized_search(l+1,r-1,s,dp);
}
else
{
ans=max(memorized_search(l+1,r,s,dp),memorized_search(l,r-1,s,dp));
}
dp[l][r]=ans;
return ans;
}
//动态规划
int dynamic_programming(int n,string &s)
{
vector<vector<int>>dp(n,vector<int>(n));
//初始化
dp[n-1][n-1]=1;
for(int i=0;i<n-1;i++)
{
dp[i][i]=1;
dp[i][i+1]=s[i]==s[i+1]?2:1;
}
for(int i=n-2;i>=0;i--)
{
for(int j=i+2;j<n;j++)
{
if(s[i]==s[j])
{
dp[i][j]=2+dp[i+1][j-1];
}
else
{
dp[i][j]=max(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][n-1];
}
//空间压缩
int optimized_dp(int n,string &s)
{
vector<int>dp(n);
//初始化
dp[n-1]=1;
for(int i=n-2;i>=0;i--)
{
dp[i+1]=s[i]==s[i+1]?2:1;
int leftDown=1,backDown;
for(int j=i+2;j<n;j++)
{
backDown=dp[j];
if(s[i]==s[j])
{
dp[j]=2+leftDown;
}
else
{
dp[j]=max(dp[j],dp[j-1]);
}
leftDown=backDown;
}
}
return dp[n-1];
}
};
这个可以说是区间dp的初见了,因为是要求回文子序列,所以整体思路就是从头尾往中间考虑。
递归尝试就是当相遇了就返回1,若最后剩两个,那么若这两个相等,就构成了一个回文,返回2;否则就只算自己返回1。之后若当前两位置一样就是2+去中间递归的结果,否则就是两个只要一个的最大值。
之后是严格位置依赖的动态规划,画出dp表,首先观察尝试函数中的basecase,可以发现,只有j大于等于i的格子才有效,即上三角区域。然后可以发现对角线上的值都为1,右侧的格子取决于是否相等。之后分析严格位置依赖,可以发现每个格子依赖左侧,下方和左下的格子,那么填的顺序就是从下往上从左往右。

空间压缩就是注意存一下左下的格子即可。
5.二叉树
#include<bits/stdc++.h>
using namespace std;
const int MOD=1000000007;
typedef long long ll;
//递归 -> 超时
int recursion(int n,int m)
{
if(n==0)//空树
{
return 1;
}
if(m==0)
{
return 0;
}
int ans=0;
for(int i=0;i<n;i++)
{
ans=(ans+(recursion(i, m-1)*recursion(n-i-1, m-1))%MOD)%MOD;
}
return ans;
}
//记忆化搜索
int memorized_search(int n,int m,vector<vector<int>>&dp)
{
if(n==0)
{
return 1;
}
if(m==0)
{
return 0;
}
if(dp[n][m]!=-1)
{
return (int)dp[n][m];
}
int ans=0;
for(int i=0;i<n;i++)
{
ans=(ans+((ll)memorized_search(i, m-1,dp)*memorized_search(n-i-1, m-1,dp))%MOD)%MOD;
}
dp[n][m]=ans;
return ans;
}
//动态规划
int dynamic_programming(int n,int m)
{
vector<vector<int>>dp(n+1,vector<int>(m+1));
//初始化
for(int j=0;j<m;j++)
{
dp[0][j]=1;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
for(int k=0;k<i;k++)
{
dp[i][j]=(dp[i][j]+((ll)dp[k][j-1]*dp[i-k-1][j-1])%MOD)%MOD;
}
}
}
return dp[n][m];
}
//空间压缩
int optimized_dp(int n,int m)
{
vector<int>dp(n+1);
dp[0]=1;
for(int j=1;j<=m;j++)//先枚举列!!
{
for(int i=n;i>=1;i--)//从下往上
{
dp[i]=0;//不依赖左侧格子 -> 重置!!!
for(int k=0;k<i;k++)
{
dp[i]=(dp[i]+((ll)dp[k]*dp[i-k-1]))%MOD;
}
}
}
return dp[n];
}
int solve(int n,int m)
{
//return recursion(n,m);
//vector<vector<int>>dp(n+1,vector<int>(m+1,-1));
//return memorized_search(n,m,dp);
//return dynamic_programming(n,m);
return optimized_dp(n,m);
}
void read()
{
int n,m;
cin>>n>>m;
cout<<solve(n,m);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
read();
return 0;
}
这个题就是计算种类数的公式需要点思考。
公式就是,比如当还剩5个节点时,当前节点的两个孩子可以是0个和5个,1个和4个……5个和0个,那么总的种类数就是每个种类相乘后相加。注意,当n=0时是空树,也算一种。
之后递归尝试每次遍历当前所有情况即可。
严格位置依赖的动态规划只需要注意n=0的那一行全是1即可。

重点是空间压缩,由于每个格子依赖自己左上到自己那一行的某些格子,所以考虑将压缩的数组看作竖着的列,那么此时滚动时就要先从左往右,再从下往上,即要先枚举列。其中还要注意由于不依赖自己左侧的格子,那么每次自己左侧的格子要重置。

6.矩阵中的最长递增路径
class Solution {
public:
vector<int>move={-1,0,1,0,-1};
int longestIncreasingPath(vector<vector<int>>& matrix) {
int n=matrix.size();
int m=matrix[0].size();
int ans=0;
vector<vector<int>>dp(n,vector<int>(m));
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
//ans=max(ans,recursion(i,j,n,m,matrix));
ans=max(ans,memorized_search(i,j,n,m,matrix,dp));
}
}
return ans;
}
//递归 -> 超时
int recursion(int x,int y,int n,int m,vector<vector<int>>&matrix)
{
int next=0;
if(x-1>=0&&matrix[x][y]<matrix[x-1][y])
{
next=max(next,recursion(x-1,y,n,m,matrix));
}
if(x+1<n&&matrix[x][y]<matrix[x+1][y])
{
next=max(next,recursion(x+1,y,n,m,matrix));
}
if(y-1>=0&&matrix[x][y]<matrix[x][y-1])
{
next=max(next,recursion(x,y-1,n,m,matrix));
}
if(y+1<m&&matrix[x][y]<matrix[x][y+1])
{
next=max(next,recursion(x,y+1,n,m,matrix));
}
return next+1;
}
//记忆化搜索
int memorized_search(int x,int y,int n,int m,vector<vector<int>>&matrix,
vector<vector<int>>&dp)
{
if(dp[x][y]!=0)
{
return dp[x][y];
}
dp[x][y]=1;
for(int i=0,nx,ny;i<4;i++)
{
nx=x+move[i];
ny=y+move[i+1];
if(nx>=0&&nx<n&&ny>=0&&ny<m&&matrix[x][y]<matrix[nx][ny])
{
dp[x][y]=max(dp[x][y],memorized_search(nx,ny,n,m,matrix,dp)+1);
}
}
return dp[x][y];
}
};
这个题也属于没法改成动态规划的,原因就是每个格子都依赖自己上下左右四个方向,没有严格位置依赖。
那么思路就是以每个点为起点去跑递归。每次都去四个方向比自己大的格子递归即可。
二、更多二维动态规划题目
1.不同的子序列
class Solution {
public:
int MOD=1000000007;
int numDistinct(string s, string t) {
//return DP(s,t);
return optimized_dp(s,t);
}
//动态规划
int DP(string &s,string &t)
{
int n=s.length();
int m=t.length();
vector<vector<int>>dp(n+1,vector<int>(m+1));
for(int i=0;i<=n;i++)//有一种 -> 空字符串
{
dp[i][0]=1;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
dp[i][j]=dp[i-1][j];
if(s[i-1]==t[j-1])
{
dp[i][j]=(dp[i][j]+dp[i-1][j-1])%MOD;
}
}
}
return dp[n][m];
}
//空间压缩
int optimized_dp(string &s,string &t)
{
int n=s.length();
int m=t.length();
vector<int>dp(m+1);
dp[0]=1;
for(int i=1;i<=n;i++)
{
for(int j=m;j>=1;j--)//从右往左 -> 每次依赖左侧未更新的值
{
if(s[i-1]==t[j-1])
{
dp[j]=(dp[j]+dp[j-1])%MOD;
}
}
}
return dp[m];
}
};
这个题的分析就比较简单了,还是考虑结尾,就是不要当前位置和如果相等就要当前位置这两种情况相加。
那么basecase就是当目标字符串是空串时,就只有一种情况,即子序列是空串。
空间压缩时,需要注意,由于每个格子依赖左侧未更新的值,所以要从右往左更新!!!
2.编辑距离
class Solution {
public:
int minDistance(string word1, string word2) {
int n=word1.length();
int m=word2.length();
//return DP(n,m,word1,word2);
return optimized_dp(n,m,word1,word2);
}
//动态规划
int DP(int n,int m,string &word1,string &word2)
{
vector<vector<int>>dp(n+1,vector<int>(m+1));
for(int i=1;i<=n;i++)
{
dp[i][0]=i;
}
for(int j=1;j<=m;j++)
{
dp[0][j]=j;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
int p1=word1[i-1]==word2[j-1]?dp[i-1][j-1]:INT_MAX;//直接用
int p2=word1[i-1]!=word2[j-1]?dp[i-1][j-1]+1:INT_MAX;//替换
int p3=dp[i][j-1]+1;//插入
int p4=dp[i-1][j]+1;//删除
dp[i][j]=min(min(p1,p2),min(p3,p4));
}
}
return dp[n][m];
}
//空间压缩
int optimized_dp(int n,int m,string &word1,string &word2)
{
vector<int>dp(m+1);
for(int j=0;j<=m;j++)
{
dp[j]=j;
}
for(int i=1;i<=n;i++)
{
int leftUp=i-1,backUp;
dp[0]=i;//注意dp[0]和leftUp的初始化!!
for(int j=1;j<=m;j++)
{
backUp=dp[j];
int p1=word1[i-1]==word2[j-1]?leftUp:INT_MAX;
int p2=word1[i-1]!=word2[j-1]?leftUp+1:INT_MAX;
int p3=dp[j-1]+1;
int p4=dp[j]+1;
dp[j]=min(min(p1,p2),min(p3,p4));
leftUp=backUp;
}
}
return dp[m];
}
};
这个题还是从结尾考虑,然后去讨论不同情况的位置依赖。那么就是当让当前字符参与的话,若想让当前字符变成目标字符,如果和目标字符一样,那就可以直接用,不消耗代价;若不一样,那就可以考虑替换,代价加一;若不让当前字符变成目标字符,那么就是插入一个字符;还有,若不让当前字符参与,直接删除即可。最后这四种情况取最小值即可。那么basecase就是若目标串为空串,那么当前串的所有字符都要删除;若当前串是空串,那么就要全插入。
空间压缩时要注意的就是dp[0]和leftUp的初始化。
3.交错字符串
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
int n=s1.length();
int m=s2.length();
if(n+m!=s3.length())
{
return false;
}
//return DP(n,m,s1,s2,s3);
return optimized_dp(n,m,s1,s2,s3);
}
//动态规划
bool DP(int n,int m,string &s1,string &s2,string &s3)
{
vector<vector<bool>>dp(n+1,vector<bool>(m+1));
dp[0][0]=true;
for(int i=1;i<=n;i++)
{
if(s1[i-1]!=s3[i-1])
{
break;
}
dp[i][0]=true;
}
for(int j=1;j<=m;j++)
{
if(s2[j-1]!=s3[j-1])
{
break;
}
dp[0][j]=true;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
bool p1=s1[i-1]==s3[i+j-1]&&dp[i-1][j];
bool p2=s2[j-1]==s3[i+j-1]&&dp[i][j-1];
dp[i][j]=p1||p2;
}
}
return dp[n][m];
}
//空间压缩
bool optimized_dp(int n,int m,string &s1,string &s2,string &s3)
{
vector<bool>dp(m+1);
dp[0]=true;
for(int j=1;j<=m;j++)
{
if(s2[j-1]!=s3[j-1])
{
break;
}
dp[j]=true;
}
for(int i=1;i<=n;i++)
{
dp[0]=s1[i-1]==s3[i-1]&&dp[0];//注意dp[0]的更新!!!
for(int j=1;j<=m;j++)
{
bool p1=s1[i-1]==s3[i+j-1]&&dp[j];
bool p2=s2[j-1]==s3[i+j-1]&&dp[j-1];
dp[j]=p1||p2;
}
}
return dp[m];
}
};
这个题还是考虑结尾,之后再去分析情况。那么情况就是,第一个情况就是让s1当前字符去拼,那么就是若s1的当前字符等于s3的当前字符,且用s1的之前字符能拼出当前s3;另一个情况就是让s2的当前字符去拼,那么就是s2当前字符是否等于且用s2之前字符能拼出来。所以最终结果就是两种可能性或起来。那么basecase就是若s1或s2分别为空时,只要和s3对上了就是true,只要出现不一样之后就都是false。
空间压缩时要注意的就是dp[0]的更新!!
4.有效涂色问题
给定n,m两个参数,一共有n个格子,每个格子可以涂上一种颜色,颜色在m种里选。当涂满n个格子,并且m种颜色都使用了,为一种有效方法。求一共有多少种有效方法。
数据范围:1<=n,m<=5000,答案对1000000007取模。
#include<bits/stdc++.h>
using namespace std;
const int MOD=1000000007;
typedef long long ll;
int f(int i,int n,int m,vector<int>&path,vector<bool>&set)
{
if(i==n)
{
fill(set.begin(),set.end(),false);
int colors=0;
for(int c:path)
{
if(!set[c])
{
set[c]=true;
colors++;
}
}
return colors==m?1:0;
}
else
{
int ans=0;
for(int j=1;j<=m;j++)
{
path[i]=j;
ans=(ans+f(i+1,n,m,path,set))%MOD;
}
return ans;
}
}
int ways1(int n,int m)
{
vector<int>path(n);
vector<bool>set(m+1);
return f(0,n,m,path,set);
}
int ways2(int n,int m)
{
vector<vector<int>>dp(n+1,vector<int>(m+1));
for(int i=1;i<=n;i++)
{
dp[i][1]=m;
}
for(int i=2;i<=n;i++)
{
for(int j=2;j<=m;j++)
{
dp[i][j]=((ll)dp[i-1][j]*j)%MOD;
dp[i][j]=(dp[i][j]+((ll)dp[i-1][j-1]*(m-j+1)%MOD))%MOD;
}
}
return dp[n][m];
}
void read()
{
cout<<"Start testing ans"<<endl;
int n=9;
int m=9;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
int ans1=ways1(i,j);
int ans2=ways2(i,j);
if(ans1!=ans2)
{
cout<<"Wrong!"<<endl;
}
}
}
cout<<"Test ends"<<endl;
cout<<"Start testing time"<<endl;
n=5000;
m=4877;
auto start=chrono::high_resolution_clock::now();
ways2(n,m);
auto end=chrono::high_resolution_clock::now();
chrono::duration<double>elapsed=end-start;
cout<<"DP costs "<<elapsed.count()<<" seconds"<<endl;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
read();
return 0;
}
高中的排列组合还在追我。(悲)
这个题的情况讨论就比较简单了,就是若前一个格子已经涂了j种颜色,那么这个格子就可以随便涂,情况数就是上一个的情况数乘以j;若上一个格子涂了j-1种,那么当前格子就有m-(j-1)种涂法。总的情况数就是这两个相加。
5.删除至少几个字符可以变成另一种字符串的子串
给定两个字符串s1和s2,返回s1至少删除多少字符可以成为s2的子串。
#include<bits/stdc++.h>
using namespace std;
string randomString(int n,int v)
{
string ans;
for(int i=0;i<n;i++)
{
ans+='a'+rand()%v;
}
return ans;
}
int f(string &s1,int i,string path,vector<string>&list)
{
if(i==s1.length())
{
list.push_back(path);
}
else
{
f(s1,i+1,path,list);
path+=s1[i];
f(s1,i+1,path,list);
}
}
int ways1(string s1,string s2)
{
vector<string>list;
string path;
f(s1,0,path,list);
sort(list.begin(),list.end(),
[&](string &a,string &b){return a.length()>b.length();});
for(string str:list)
{
if(s2.find(str)!=string::npos)
{
return s1.length()-str.length();
}
}
return s1.length();
}
int ways2(string s1,string s2)
{
int n=s1.length();
int m=s2.length();
//dp[i][j] -> s1前i个字符至少删掉几个字符能变成s2前j个字符的任意后缀串
vector<vector<int>>dp(n+1,vector<int>(m+1));
for(int i=1;i<=n;i++)
{
dp[i][0]=i;
for(int j=1;j<=m;j++)
{
if(s1[i-1]==s2[j-1])
{
dp[i][j]=dp[i-1][j-1];
}
else
{
dp[i][j]=1+dp[i-1][j];
}
}
}
//返回最后一行的最小值
int ans=INT_MAX;
for(int j=0;j<=m;j++)
{
ans=min(ans,dp[n][j]);
}
return ans;
}
void read()
{
srand(time(0));
int n=12;
int v=3;
int testTime=20000;
cout<<"Start testing ans"<<endl;
for(int i=0;i<testTime;i++)
{
int len1=rand()%n+1;
int len2=rand()%n+1;
string s1=randomString(n,v);
string s2=randomString(n,v);
int ans1=ways1(s1,s2);
int ans2=ways2(s1,s2);
if(ans1!=ans2)
{
cout<<"Wrong"<<endl;
}
}
cout<<"Test ends"<<endl;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
read();
return 0;
}
这个题的思路就属于没有那点灵机一动打死也想不出来的……
最逆天的就是这个对dp表的定义。dp[i][j]在这里定义为s1的前i个字符至少删掉几个能变成s2的前j个字符的任意后缀串,之后答案要取dp表最后一行的最小值……
那么有了这个定义就好分析情况了,就是若两位置相等就不用删,否则就删一次即可。
总结
这么看下来,动态规划的核心就是分析情况。当遇到子序列或者子串问题时,一定要记得优先考虑结尾。在空间压缩时,最保险最直观的方法还是画出dp表,然后对照dp表去考虑压缩时的更新细节。
END
更多推荐



所有评论(0)