0
点赞
收藏
分享

微信扫一扫

DP系列刷题中问题小总结(持续更新中)

火热如冰 2022-03-14 阅读 34

DP系列刷题中问题小总结

概述

DP一种常用的算法思想,可难可易,非常灵活。

解决通法:

求解DP问题主要有三步:(废话

  1. 定义状态
  2. 状态转移
  3. 算法实现

对于DP问题,主要需要找出状态的定义以及状态的转移。

DP问题全家桶(苟若刷题遇见的题型)

  • [简单DP] (and复杂DP 🐶)
  • 区间DP
  • 树形DP
  • 数位DP
  • 状压DP
  • 递推

以下内容为本人(苟若)在学习DP过程中的一些小总结。

简单DP

​ 简单DP是一些经典的问题,一般可以很直观的表示并转移状态。

1. 硬币问题

1.1 最少硬币问题
Problem Description:

设有n种不同面值的硬币,现要用这些面值的硬币来找钱。硬币可以使用无限次,有T组输入,每次输入一个m,对任意钱数0≤m≤20001,设计一个用最少硬币找钱m的方法。

解题思路:考虑本问题所求属性为最少的硬币找钱。

假设硬币为[1,5,10,15,20],需要找钱数目为m, 那么m可以由什么状态转移过来呢?

很显然,m=m-1+1=m-5+5=m-10+10=......,问题就转换成了找钱m-1,m-5.....的子问题。

显而易见的,我们可以定义状态dp[i]:找钱数为i所需要的最少硬币。

状态转移: dp[i]=dp[i-money_type]+1

由于有多种途径,所求属性为最小值,则

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

Solve-Code:

#include<iostream>
#include<cstring>
using namespace std;
#define INF 0x3f3f3f3f
const int N = 1010;//假定硬币可能种类,反正超不了
const int Money = 20010;//最大找钱数目,反正超不了
int money_type[N];
int dp[Money];//对不同的钱数的结果,俗称---打表
int n;//n种钱币
void initial() {
	for (int i = 0; i < Money; i++)
		dp[i] = INF;
	dp[0] = 0;
	for (int i = 1; i <= n; i++) {
		for (int j = money_type[i]; j < Money; j++) {
			dp[j] = min(dp[j], dp[j - money_type[i]] + 1);
		}
	}
}
int main() {
	int T,m;
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> money_type[i];
	}
	cin >> T;
	initial();
	for (int i = 0, x; i < T; i++) {
		cin >> x;
		cout << dp[x] << endl;
	}
	return 0;
}
求具体方案

对于具体方案,显然的,我们可以在状态转移的过程中添加转移路径(即方案),在更新最小值的同时同步更新选择的硬币面值。

Solve-Code:

#include<iostream>
#include<cstring>
using namespace std;
#define INF 0x3f3f3f3f
const int N = 1010;//假定硬币可能种类,反正超不了
const int Money = 20010;//最大找钱数目,反正超不了
int money_type[N];
int dp[Money];//对不同的钱数的结果,俗称---打表
int n;//n种钱币
int dp_path[Money] = { 0 };
void initial() {
	for (int i = 0; i < Money; i++)
		dp[i] = INF;
	dp[0] = 0;
	for (int i = 1; i <= n; i++) {
		for (int j = money_type[i]; j < Money; j++) {
			if (dp[j] > dp[j - money_type[i]] + 1) {//需要转移
				dp[j] = dp[j - money_type[i]] + 1;
				dp_path[j] = money_type[i];
			}
		}
	}
}
void Print(int s) {//输出方案
	while (s) {
		cout << dp_path[s] << ' ';
		s -= dp_path[s]; //回到上一个状态
	}
}
int main() {
	int T,m;
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> money_type[i];
	}
	cin >> T;
	initial();
	for (int i = 0, x; i < T; i++) {
		cin >> x;
		cout << dp[x] << endl;
		cout << "组成方案为:" << endl;
		Print(x);
		cout << endl;
	}
	return 0; 
}
1.2 所有方案硬币的组合
Problem Description:

设有n种不同面值的硬币,现要用这些面值的硬币来找钱。硬币可以使用无限次,有T组输入,每次输入一个m,对任意钱数0≤m≤20001,求用硬币找钱m的总方案数。

解题思路:考虑本题所求属性为方案总数

1.1相信你一定有所感悟,在不断更新最小值的过程中不是已经枚举出来了所有方案的呀!

于是状态表示 dp[i]:钱数为i的找钱方案,显然当需要找钱数为m时,有面值为1,5时,面值m的方案为m-1与·m-5方案的总和。

转移当然是和上面(一样样):dp[i]+=dp[i-money_type]

上述对代码的修改很少


dp初始化为0 ,dp[0]=1, 状态转移过程中 dp[j]+=dp[j-money_type[i]]

#include<iostream>
#include"cstring"
using namespace std;
#define INF 0x3f3f3f3f
const int N = 1010;//假定硬币可能种类,反正超不了
const int Money = 20010;//最大找钱数目,反正超不了
int money_type[N];
int dp[Money];//对不同的钱数的结果,俗称---打表
int n;//n种钱币
void initial() {
	dp[0] = 1;
	for (int i = 1; i <= n; i++) {
		for (int j = money_type[i]; j < Money; j++) {
			dp[j] += dp[j - money_type[i]];
		}
	}
}
int main() {
	int T,m;
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> money_type[i];
	}
	cin >> T;
	initial();
	for (int i = 0, x; i < T; i++) {
		cin >> x;
		cout << dp[x] << endl;
	}
	return 0;
}
1.3 背包问题

背包问题是DP下又一大类子问题。

1.3.1 01背包
Problem Description:有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

动态规划是不断决策求最优解的过程,0-1 背包即是不断对第 i 个物品的做出决策,0-1正好代表不选与选两种决定。而维度有当前背包,以及背包的容量。

考虑本题所求属性:最大价值

状态dp[i][j]定义:前 i个物品,背包容量 j 下的最优解(最大价值):

状态转移:选:dp[i][j] = dp[i - 1][j - v[i]] + w[i](若选择了该物品,自然需要为该物品提供出空间)
不选:dp[i][j] = dp[i - 1][j](未选择该物品,不需要为该物品提供空间)

Solve-Code:

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
using namespace std;
const int MAXN = 1005;
int v[MAXN];    // 体积
int w[MAXN];    // 价值 
int dp[MAXN][MAXN];  // dp[i][j], 前i个物品j体积的最大价值 
int main() {
    int n, m;   
    cin >> n >> m;
    for(int i = 1; i <= n; i++) 
        cin >> v[i] >> w[i];
    for(int i = 1; i <= n; i++) 
        for(int j = 1; j <= m; j++){
            if(j < v[i]) 
                dp[i][j] = dp[i - 1][j]; //  当前背包容量装不进第i个物品,则价值等于前i-1个物品
            else    
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);//  // 能装,需进行决策是否选择第i个物品
        }           
    cout << dp[n][m] << endl;
    return 0;
}

01背包问题可以降为一维动态规划,想了解的朋友可以参见

01背包问题的一维优化

1.3.2 完全背包问题
Problem Description:有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。第 i 种物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

这里仍然采用最朴素的三维思路:对于每一个物品,我们需要加入物品选择数量的考量,故会引入第三重循环去遍历装入该物品的可能数量去更新dp[i][j]由于思路较为简单,不加赘述.

Solve-Code:

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
using namespace std;
const int N = 10010;
int dp[N][N];
int v[N],w[N];
int main(){
    int n,m;
    cin>>n>>m;
    for(int i = 1 ; i <= n ;i ++){
        cin>>v[i]>>w[i];
    }
    for(int i = 1 ; i<=n ;i++)
      for(int j = 0 ; j<=m ;j++) {
        for(int k = 0 ; k*v[i]<=j ; k++)
            dp[i][j] = max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);
    }
   cout <<  dp[n][m];
    return 0;
}

完全背包问题的一维优化

除此之外还有多重背包问题、混合背包问题、二位费用背包问题、分组背包问题、有依赖的背包问题、背包求方案数的问题、背包求具体方案问题。

背包问题是典型的DP问题,读者应当尝试自己动手完成系列问题的推导与求解。

这里引用经典博文详细介绍了背包问题的求解。

背包九讲

背包问题较多,选取部分一维优化代码请参见:

背包问题

1.4 最长公共子序列

Problem Description:对于序列X与序列Y,求X与Y的公共子序列

状态表示:dp[i][j]:子序列Xi段与子序列Yj段的最长公共子序列的长度,显而易见的,dp[X.lenth][Y.lenth]即为所求解。

状态转移:

dp[i][j]=dp[i-1][j-1]+1 if(X[i]==Y[j])当两者相等时,显然我们可以[i-1,j-1]像后扩展一位

dp[i][j]=max(dp[i][j-1],dp[i+1][j]) if(X[i]!=Y[j])当两者不相等时,可以尝试独立加入X[i],此时为dp[i][j-1]

独立加入Y[j],此时为dp[i-1][j],得出转移方程。

Solve-Code:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int N = 1000;
int dp[N][N];
int main() {
	string X,Y ;
	cin >> X >> Y;
	X = " " + X;
	Y = " " + Y; //小东西便于代码编写
	for(int i=1;i< X.size();i++)
		for (int j = 1; j < Y.size(); j++) {
			if (X[i] == Y[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
			else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
		}
	for (int i = 1; i < X.size(); i++) {
		for (int j = 1; j < Y.size(); j++) {
			cout << dp[i][j] << ' ';  //打印各部分状态,得到转移表
		}
		cout << endl;
	}
	cout << f[X.size()-1][Y.size()-1];
	return 0;
}

1.5 最长公共子串

最长公共子序列和最长公共子串不是一回事儿。子序列即一个给定的序列的子序列,就是将给定序列中零个或多个元素去掉之后得到的结果。子串是给定串中任意个连续的字符组成的子序列称为该串的子串。

Problem Description:对于字符串S1与序列S2,求S1与S2的最长公共子序列

求解属性:公共子串最长长度,对于S1与S2各含一个维度。

状态表示:f[i][j]:以s1[i]s2[j]作为结尾的最长公共子串长度

状态转移:

		if (s1[i] == s2[j]) f[i][j] = f[i-1][j-1] + 1;  //可以向后扩展
		else f[i][j] = max(0, f[i][j]);  //注意与子序列不同,子串必须保持连续,否则f[i][j]=0;

Solve-Code

//最长公共子串 s1,s2;
//f[i][j]:以s1[i]和s2[j]作为结尾的最长公共子串长度
#include<iostream>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int N = 10000;
int f[N][N];
int main() {
	string s1, s2;
	cin >> s1 >> s2;
	int n = s1.size(), m = s2.size();
 //递推下标是从1开始的
	for (int i = 0; i < n; i++)
		if (s2[0] == s1[i]) f[i][0] = 1;
	for (int j = 0; j < m; j++)
		if (s1[0] == s2[j]) f[0][j] = 1;
	for(int i=1;i<n;i++)
		for (int j = 1; j < m; j++) {
			if (s1[i] == s2[j]) f[i][j] = f[i-1][j-1] + 1;
			else f[i][j] = max(0, f[i][j]);
		}
	cout << f[n-1][m-1];
	return 0;
}

作者修养生息中准备下一章…

小试牛刀

想练习可以AC以下题目:

Coin Change

超级楼梯

Robberies

最大报销额

举报

相关推荐

0 条评论