DP动态规划问题经典题目入门(含思路及ac代码)

DP


一点心得:

将一个问题拆成几个子问题,分别求解这些子问题,即可推断出大问题的解

当f[i]需要用到f[i-1]等更小的dp方程时,不要多虑,此时的f[i-1]一定已经根据之前的求出来了,大胆去用!!!此处类似于递归 dfs…


dp也是我学习算法路上的一只强劲的拦路虎,我也只是初窥门径,还需不断学习并积累。以下为备战icpc省赛时所学习并整理的典题,包含题目思路及多种ac代码,后续也会不断学习和补充dp相关的笔记思路和代码,希望能帮助到刚刚入门dp的同学。


目录:

  • 背包问题
  • 线性DP
  • 区间DP
  • 树形DP

背包问题

物品属性(vi,wi,个数限制) 和 背包容量(V)

1. 01背包

 每件物品选0or1次

 理解:

 f(i,j)表示所有只从前i个物品中选 且总体积<=j的选法的集合
 存的数是里面每个集合里面每个选法的w的最大值 

 f[i][j]有两种情况:
 第一种不包含第i个物品 此时 f[i][j]=f[i-1][j]; 一定存在 
 第二种包含第i个物品 此时f[i][j]=f[i-1][j-v[i]]+w[i];当w[i]<=j时存在

 二维做法:
 #include<bits/stdc++.h>
 using namespace std;
 const int N = 1010;
 int v[N],w[N];
 int f[N][N];//f[i][j] 表示 所有满足条件在前i件物品中选择且总体积不超过j的选法 中的Wmax
 int main()
 {
     int n,V;
     cin>>n>>V;
     
     for(int i = 1;i <= n;i ++) cin>>v[i]>>w[i];
     
     for(int i=1;i <= n;i ++)
     {
         for(int j=1;j <= V;j ++)
         {
             f[i][j]=f[i-1][j];
             if(j>=v[i])
             f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
         }
     }
     cout<<f[n][V];//所有满足条件在前n件物品中选择且总体积不超过V的选法 中的Wmax
     return 0;
 
 }

 一维做法:
 #include<bits/stdc++.h>
 using namespace std;
 const int N = 1010;
 int v[N],w[N];
 int f[N];
 int main()
 {
     int n,V;
     cin>>n>>V;
     
     for(int i = 1;i <= n;i ++) cin>>v[i]>>w[i];
     
     for(int i=1;i <= n;i ++)
     {
         for(int j=V;j >= v[i];j --)
         {
             
             f[j]=max(f[j],f[j-v[i]]+w[i]);
         }
     }
     cout<<f[V];
     return 0;
 }

2. 完全背包

 每件物品可以选无限个

 三维:
 #include<bits/stdc++.h>
 using namespace std;
 const int N = 1010;
 int v[N],w[N];
 int f[N][N];
 
 int main()
 {
     int n,V;
     cin>>n>>V;
     
     for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
     
     for(int i=1;i<=n;i++)
         for(int j=1;j<=V;j++)
             for(int k=0;k*v[i]<=j;k++)
             {
                 f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
             }
             
     cout<<f[n][V];
     return 0;
 }
 
 二维:
 #include<bits/stdc++.h>
 using namespace std;
 const int N = 1010;
 int f[N][N];
 int v[N],w[N];
 
 int main()
 {
     int n,V;
     cin>>n>>V;
     
     for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
     
     for(int i=1;i<=n;i++) 
         for(int j=1;j<=V;j++)
         {
             f[i][j]=f[i-1][j];
             if(v[i]<=j)
             f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
         }
         
     cout<<f[n][V];
     return 0;
 }
 

 截止到此处,不难发现与01背包二维相似,故类比可优化成一维


 一维:
 #include<bits/stdc++.h>
 using namespace std;
 const int N = 1010;
 int f[N];
 int v[N],w[N];
 
 int main()
 {
     int n,V;
     cin>>n>>V;
     
     for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
     
     for(int i=1;i<=n;i++) 
         for(int j=v[i];j<=V;j++)
         {
             f[j]=max(f[j],f[j-v[i]]+w[i]);
         }
         
     cout<<f[V];
     return 0;
 }
 ```

3. 多重背包(朴素版&&优化版)

 每件物品最多有Si个

 朴素版(形似于完全背包三维):
 #include<bits/stdc++.h>
 using namespace std;
 const int N =110;
 int f[N][N];
 int v[N],s[N],w[N];
 
 int main()
 {
     int n,V;
     cin>>n>>V;
     for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i];
     
     for(int i=1;i<=n;i++)
     {
         for(int j=1;j<=V;j++)
         {
             for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
             {
                 f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+w[i]*k);
             }
         }
     }
     
     cout<<f[n][V];
     return 0;
 }




 格式虽形似完全背包优化方式

 但在多重背包问题中受到背包大小及物品数量si所限制,故f[i] [j-v] 比f[i] [j] 后s项可能要多一项 故无法代换为f[i] [j-v]+w。

 而完全背包只受背包大小因素限制,故f[i] [j-v]的末项一定等同于f[i] [j]后k项,故可通过状态转移方程f[i] [j]=max(f[i-1] [j],f[i] [j-v]+w)所代换

 二进制优化:
 //一种物品(s份)分为log2s份 每份看作一个新物品 可转化成01背包问题
 //原本需要枚举s次 现在通过二进制分割新物品只需要枚举log2s次即可
 #include<bits/stdc++.h>
 using namespace std;
 const int N=12*1010;//按照二进制分组后的新种类数
 const int M=2010;//种类数
 int v[N],w[N],s[N];
 int f[M];//状态方程
 int n,m;
  
 int main()
 {
 	cin>>n>>m;
 	int cnt=0;
 	for(int i=1;i<=n;i++)//n种 
 	{
 		int a,b,x;
 		cin>>a>>b>>x;
 		int k=1; 
 		while(k<=x)
 		{
 			cnt++;
 			v[cnt]=k*a;
 			w[cnt]=k*b;
 			x-=k;
 			k*=2;
 		}
 		if(x>0)
 		{
 			cnt++;
 			v[cnt]=x*a;
 			w[cnt]=x*b;	
 		}
 	}
 	
 	n=cnt;//更新物品数n
     
 	//此时优化成01背包问题 
 	for(int i=1;i<=n;i++)
 	 	for(int j=m;j>=v[i];j--)
 	 		f[j]=max(f[j],f[j-v[i]]+w[i]);
 	
 	cout<<f[m];
 	
 	return 0;
 } 

4. 分组背包

 N组物品,每组有若干个物品,每一组最多选一个物品(水果:香蕉 苹果 葡萄...)

 二维:
 #include<bits/stdc++.h>
 using namespace std;
 const int N = 110;
 int f[N][N];
 int v[N][N],s[N],w[N][N];
 int main()
 {
 	int n,m;
 	cin>>n>>m;
 	
 	for(int i=1;i<=n;i++)
 	{
 		cin>>s[i];
 		for(int j=1;j<=s[i];j++)
 			cin>>v[i][j]>>w[i][j];//第i组的第j个物品的数据 
 	}
 	
 	for(int i=1;i<=n;i++)
 	{
 		for(int j=1;j<=m;j++)
 		{
 		    //f[i][j]=f[i-1][j];
 			for(int k=0;k<=s[i];k++)
 			{
 				if(v[i][k]<=j)
 				f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
 			}
 		}
 	}
 	cout<<f[n][m];
 } 

 优化:
 #include<bits/stdc++.h>
 using namespace std;
 const int N = 110;
 int f[N];
 int v[N][N],s[N],w[N][N];
 int main()
 {
 	int n,m;
 	cin>>n>>m;
 	
 	for(int i=1;i<=n;i++)
 	{
 		cin>>s[i];
 		for(int j=1;j<=s[i];j++)
 			cin>>v[i][j]>>w[i][j];//第i组的第j个物品的数据 
 	}
 	
 	for(int i=1;i<=n;i++)
 	{
 		for(int j=m;j>=0;j--)
 		{
 			for(int k=0;k<=s[i];k++)
 			{
 				if(v[i][k]<=j)
 				f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
 			}
 		}
 	}
 	cout<<f[m];
 } 

线性DP

1. 数字三角形

 #include<bits/stdc++.h>
 using namespace std;
 const int N = 510;
 int a[N][N];//存储数据
 int f[N][N];//f[i][j]表示所有最后到达(i,j)的路径 的集合 的数字和 的max
 const int INF = 0x3f3f3f3f;
 int main()
 {
     int n;
     cin>>n;
     for(int i=1;i<=n;i++)
         for(int j=1;j<=i;j++)
             cin>>a[i][j];
             
     for(int i=0;i<=n;i++)
         for(int j=0;j<=i+1;j++)
             f[i][j]=-INF;//初始化 此处仔细思考一下边界问题
         
     f[1][1]=a[1][1];    
     for(int i=2;i<=n;i++)
         for(int j=1;j<=i;j++)
             f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);//由左上角或者右上角转化而来 取max作为f[i][j]的值
     
     int res=-INF;
     for(int i=1;i<=n;i++) res=max(res,f[n][i]);//依次遍历f[n][i] 取最大值则为最大的数字和
     cout<<res;
 }

2. 最长上升子序列

 #include<bits/stdc++.h>
 using namespace std;
 const int N = 1010;
 int a[N];
 int f[N];//以第i个数结尾的所有上升子序列的集合 的长度 的max
 int mem[N];//存储以i结尾的上升子序列倒数第二位的数字的位置 即由谁转化而来
 int main()
 {
     int n;
     cin>>n;
     for(int i=1;i<=n;i++) cin>>a[i];
     
     for(int i=1;i<=n;i++)
     {
         f[i]=1;//默认上升子序列只有第i个数本身
         mem[i]=0;//此时没有上一位数字
         for(int j=1;j<i;j++)
         {
             if(a[j]<a[i])
                 if(f[i]<f[j]+1)
                 {
                     f[i]=f[j]+1;
                     mem[i]=j;//f[i]由j转移过来的 记录一下
                 }
         }
     }
     
     int idx=1;
     for(int i=1;i<=n;i++)
         if(f[idx]<f[i]) idx=i;//比较f[]
     cout<<f[idx]<<endl;
     
     //倒序输出最长上升子序列
     for(int i=0,len=f[idx];i<len;i++)
     {
         cout<<a[idx]<<' ';
         idx=mem[idx];
     }
     
     return 0;
 } 


 优化版:
 #include<bits/stdc++.h>
 using namespace std;
 const int N = 1e5+10;
 int n;
 int a[N];
 int q[N];//q[i]存储长度为i的上升子序列的最小结尾数字 
 
 int main()
 {
     scanf("%d",&n);
     for(int i=0;i<n;i++) scanf("%d",&a[i]);
     
     int len=0;//用于记录最长上升子序列的长度
     q[0]=-2e9;
     for(int i=0;i<n;i++)
     {
         int l=0,r=len;
         while(l<r)
         {
             int mid=l+r+1>>1;
             if(q[mid]<a[i]) l=mid;
             else r=mid-1;
         }
         len=max(len,r+1);//r是找到的小于a[i]的数里面的最大值 将a[i]排在第r+1位 故以a[i]结尾的最长上升子序列长度为r+1 取max
         //因为q[r]为小于a[i]的数里面的最大值,故q[r+1]>=a[i] 用a[i]替换q[r+1];
         q[r+1]=a[i];
     }
     
     printf("%d",len);
     return 0;
 }
 

3. 最长公共子序列

 /*划分四个区间 00 10 01 11
 可用dp[i-1][j-1] dp[i][j-1] dp[i-1][j] dp[i-1][j-1]+1 
 其中不含a[i],b[j]即dp[i-1][j-1]可以被dp[i][j-1] dp[i-1][j]代替
 而10 01情况无法直接表示 但是dp[i][j-1] dp[i-1][j]包含10 01情况 且为dp[i][j]的子集 所以可以代替
 */
 #include<bits/stdc++.h>
 using namespace std;
 const int N = 1010;
 int dp[N][N];
 
 int main()
 {
     string a,b;
     int n,m;
     cin>>n>>m>>a>>b;
     
     for(int i=1;i<=n;i++)
         for(int j=1;j<=m;j++)
         {
             dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
             if(a[i-1]==b[j-1]) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
         }
     
     cout<<dp[n][m];
     return 0;
 }

4. 最短编辑距离

 #include<bits/stdc++.h>
 using namespace std;
 const int N = 1010;
 char a[N],b[N];
 int f[N][N];//所有将a[1-i]变为b[1-j]的操作方式 的 操作次数 的 min
 //可分为 删:f[i-1][j]+1 增:f[i][j-1]+1 改:f[i-1][j-1]+1|0
 int main()
 {
     int n,m;
     cin>>n>>a+1>>m>>b+1;
     int p=0;
     
     for(int i=0;i<=m;i++) f[0][i]=i;
     for(int i=0;i<=n;i++) f[i][0]=i;
     
     for(int i=1;i<=n;i++)
     {
         for(int j=1;j<=m;j++)
         {
             f[i][j] = min(f[i-1][j]+1,f[i][j-1]+1);
             if(a[i]==b[j]) p=0;
             else p=1;
             f[i][j] = min(f[i][j],f[i-1][j-1]+p);
         }
     }
     
     cout<<f[n][m];
     
     return 0;
 }

5. 编辑距离

 #include<bits/stdc++.h>
 using namespace std;
 const int N = 1010;
 char a[N][15],b[15];
 int f[15][15];//所有将a[1-i]变为b[1-j]的操作方式 的 操作次数 的 min
 //可分为 删:f[i-1][j]+1 增:f[i][j-1]+1 改:f[i-1][j-1]+1|0
 int main()
 {
 	int n,m;
 	cin>>n>>m;
 	for(int i=0;i<n;i++) cin>>a[i]+1; 
 
     //for(int i=0;i<=15;i++) f[0][i]=f[i][0]=i;
     while(m--)
     {
         int p;
         cin>>b+1>>p;
         int cnt=0;
         for(int i=0;i<n;i++)
         {
            
             int lena=strlen(a[i]+1),lenb=strlen(b+1);
              for(int i=0;i<=lenb;i++) f[0][i]=i;
              for(int i=0;i<=lena;i++) f[i][0]=i;
             //cout<<"lena = "<<lena<<' '<<"lenb = "<<lenb<<endl;
             for(int j=1;j<=lena;j++)
                 for(int k=1;k<=lenb;k++)
                 {
                     f[j][k] = min(f[j-1][k]+1,f[j][k-1]+1);
                     if(a[i][j]==b[k]) f[j][k] = min(f[j][k],f[j-1][k-1]);
                     else f[j][k] = min(f[j][k],f[j-1][k-1]+1);
                     //printf("f[%d][%d] = %d\n",j,k,f[j][k]);
                 }
             if(f[lena][lenb]<=p)cnt++;
             //cout<<f[lena][lenb]<<endl;
         }
         cout<<cnt<<endl;
     }
     return 0;
 }

区间DP

1. 石子合并

 #include<bits/stdc++.h>
 using namespace std;
 const int N = 310;
 int s[N];
 int f[N][N];//第i堆到第j堆石子合并的方式 min代价
 int main()
 {
     int n;
     cin>>n;
     
     for(int i=1;i<=n;i++) cin>>s[i];
     for(int i=1;i<=n;i++) s[i]+=s[i-1];
         
     for(int len=2;len<=n;len++)//len==1时无须合并
         for(int i=1;i+len-1<=n;i++)
         {
             int l=i,r=i+len-1;
             f[l][r]=1e9;
             for(int k=i;k<r;k++)
             {
                 f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);
             }
         }
         
     cout<<f[1][n];
     return 0;
 }

树形DP

1.没有上司的舞会

#include<bits/stdc++.h>
using namespace std;
int f[5][6000];
int n,a,b,root;
int ne[12000],po[12000],ru[12000];
void dp(int x)
{
  for(int i = po[x];i;i = ne[i])
  {
      dp(i);
      f[1][x] = max(max(f[1][x],f[0][i]+f[1][x]),f[0][i]);//这一个点选的时候转移:可以不选子节点,可以是子节点不选时最大值+自己的值,可以是只是子节点不选时的最大值
      f[0][x] = max(max(f[0][x],f[1][i]+f[0][x]),max(f[1][i],f[0][i]));//这一个点不选的时候转移:可以是自己,可以是子节点也不选,可以是子节点选时+自己,可以是仅仅子节点选时最大
  }
}
int main(){
  scanf("%d",&n);
  for(int i = 1;i <= n;i ++)
   scanf("%d",&f[1][i]);
  for(int i = 1;i <= n;i ++) 
   {
       scanf("%d%d",&b,&a);
       ru[b] ++;
       ne[b] = po[a]; //原理与邻接表类似  只是里面存储的都是点  可手动模拟一下
       po[a] = b;
   }
  for(int i = 1;i <= n;i ++)
   if(ru[i] == 0) 
     {
          root = i;//找树的根
          break;
     } 
  dp(root);   
  printf("%d",max(f[1][root],f[0][root]));
  return 0;
}
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值