动态规划经典例题

  1. 01背包问题
    有n个重量和价值分别为wi,vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。
    限制条件:1 <= n <=100, 1 <= wi, vi<=100, 1 <= W <= 10000
    首先使用最朴素的方法,针对每个物品是否放入背包进行搜索。
/*
4 5
2 3
1 2
3 4
2 2

*/
#include <iostream>
using namespace std;
int w[105], v[105], n;
int f(int i, int j)
{
	int res;
	if (i == n)
	{
		//没有剩余物品 
		res = 0;
	}
	else if (j < w[i])
	{
		//无法选 
		res = f(i + 1, j);
	}
	else
	{
		//选与不选 
		res = max(f(i + 1, j), f(i + 1, j - w[i]) + v[i]);
	}
	return res; 
}
int main()
{
	int W;
	cin >> n >> W;
	for (int i = 0; i < n; i++)
	{
		cin >> w[i] >> v[i];
	}
	cout << f(0, W) << endl;
	return 0;
} 

这种方法的搜索深度是n,而且每一层的搜索都需要两次分支,最坏就需要O(2^n)的时间,当n比较大时就没办法解了。下面是f的递归情况:
在这里插入图片描述
可以看到f(3, 2)调用了两次。下面用记忆化搜索来解这道题,只需要做一点小小的改变。
增加一个二维dp数组记录选了i个物品背包还剩余j重量,dp[i][j]的值则记录其价值。

#include <iostream>
#include <cstring>
using namespace std;
int w[105], v[105], n, dp[105][105];
int f(int i, int j)
{
	if (dp[i][j] >= 0)
	{
		return dp[i][j];
	}
	int res;
	if (i == n)
	{
		//没有剩余物品 
		res = 0;
	}
	else if (j < w[i])
	{
		//无法选 
		res = f(i + 1, j);
	}
	else
	{
		//选与不选 
		res = max(f(i + 1, j), f(i + 1, j - w[i]) + v[i]);
	}
	return dp[i][j] = res; 
}
int main()
{
	int W;
	cin >> n >> W;
	memset(dp, -1, sizeof(dp));
	for (int i = 0; i < n; i++)
	{
		cin >> w[i] >> v[i];
	}
	cout << f(0, W) << endl;
	return 0;
} 

下面我们来研究一下这个记忆化数组dp,从第i个物品开始挑选总重小于j时,有如下递推公式:
在这里插入图片描述

#include <iostream>
#include <cstring>
using namespace std;
int w[105], v[105], dp[105][105];
int main()
{
	int W, n;
	cin >> n >> W;
	for (int i = 0; i < n; i++)
	{
		cin >> w[i] >> v[i];
	}
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j <= W; j++)
		{
			if (j < w[i])
			{
				dp[i + 1][j] = dp[i][j];
			}
			else
			{
				dp[i + 1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]);
			}
		}
	}
	cout << dp[n][W];
	return 0;
} 
  1. 最长公共子序列问题
    给定两个字符串s1,s2…Sn和t1,t2…tn。求出这两个字符串最长的公共子序列的长度。字符串
    s1s2“Sn的子序列指可以表示为ss…S(ii…<ian)的序列。
    下面是状态转移方程:
    在这里插入图片描述
#include <iostream>
using namespace std;
int dp[105][105];
int main()
{
	string s, t;
	cin >> s >> t;
	for (int i = 0; i < s.length(); i++)
	{
		for (int j = 0; j < t.length(); j++)
		{
			if (s[i] == t[j])
			{
				dp[i + 1][j + 1] = dp[i][j] + 1;
			}
			else
			{
				dp[i + 1][j + 1] = max(dp[i + 1][j], dp[i][j + 1]);
			}
		}
	} 
	cout << dp[s.length()][t.length()];
	return 0;
 } 
  1. 完全背包问题
    有n个重量和价值分别为wi,vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。在这里,每个物品可以挑选任意多件。

递推关系:
在这里插入图片描述

核心代码:

for (int i = 0; i < n; i++)
{
	for (int j = 0; j <= W; j++)
	{
		for (int k = 0; k * w[i] <= j; k++)
		{
			dp[i + 1][j] = max(dp[i + 1][j], dp[i + 1][j - k * w[i]] + k * v[i]);
		}
	}
}

这里用了三重循环,时间复杂度为o(nW^2)。其实可以不用关于k的循环,用O(nW)时间就可以解决问题。下面是推导过程(我没有太看懂,记住核心代码就行了):
在这里插入图片描述
核心代码:

for (int i = 0; i < n; i++)
{
	for (int j = 0; j <= W; j++)
	{
		if (j < w[i])
		{
			dp[i + 1][j] = dp[i][j];
		}
		else
		{
			dp[i + 1][j] = max(dp[i][j], dp[i + 1][j - w[i]] + v[i])
		}
	}
}

这个代码和上面0背包问题只有一个地方不同。
在01背包当中:dp[i + 1][j] = max(dp[i][j], dp[i ][j - w[i]] + v[i])
在完全背包当中:dp[i + 1][j] = max(dp[i][j], dp[i + 1][j - w[i]] + v[i])

此前提到的01背包问题和这里的完全背包问题,可以通过不断重复利用一个数组来实现。
01背包:

for (int i = 0; i < n; i++)
{
	for (int j = W; j >= w[i]; j--)
	{
		dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
	}
}
cout << dp[W] << endl;

完全背包:

for (int i = 0; i < n; i++)
{
	for (int j = w[i]; j <= W; j++)
	{
		dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
	}
}
cout << dp[W] << endl;
  1. 背包问题之二
    有n个重量和价值分别为wi,vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。
    限制条件:1 <= n <=100, 1 <= wi <=10^7, 1 <= vi <=100, 1 <= W <= 10^9

分析:
这个问题与最初的01背包向题相比, 只修改了限制条件的大小。在这个问题中,相比较重量而言,价值的范围比较小,所以可以试着改变DP的对象。之前的方法中,我们用DP针对不同的重量限制计算最大的价值。这次不妨用DP针对不同的价值计算最小的重量。
递推方程:
在这里插入图片描述

#include <iostream>
#include <algorithm>
using namespace std;
int w[105], v[105], dp[105][105];
int main()
{
	fill(dp[0], dp[0] + 105, 0x3f3f3f3f);//不存在时都是无穷大 
	dp[0][0] = 0;//前0个物品什么都挑选不了 
	int W, n;
	cin >> n >> W;
	int MAX_V = -1;
	for (int i = 0; i < n; i++)
	{
		cin >> w[i] >> v[i];
		if (v[i] > MAX_V)
		{
			MAX_V = v[i];
		}
	}
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j <= n * MAX_V; j++)
		{
			if (j < v[i])
			{
				dp[i + 1][j] = dp[i][j];
			}
			else
			{
				dp[i + 1][j] = min(dp[i][j], dp[i][j - v[i]] + w[i]);
			}
		}
	}
	int res = 0;
	for (int i = 0; i <= n * MAX_V; i++)
	{
		if (dp[n][i] <= W)
		{
			res = i;
		}
	}
	cout << res << endl;
	return 0;
} 
  1. 多重部分和问题
    有n种不同大小的数字ai,每种各mi个。判断是否可以从这些数字之中选出若干使它们的和恰好为K。

分析:dp[i +1][j]=用前种数字是否能加和成j。

#include <iostream>
/*
3 17
3 3
5 2
8 2
*/
using namespace std;
int a[105], m[105];
int dp[105][105];
int main()
{
	int n, k;
	cin >> n >> k;
	for (int i = 0; i < n; i++)
	{
		cin >> a[i] >> m[i];
	}
	dp[0][0] = 1;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j <= k; j++)
		{
			for (int k = 0; k <= m[i]; k++)
			{
				if (j >= k * a[i])
				{
					dp[i + 1][j] |= dp[i][j - k * a[i]];
				}
			}
		}
	}
	if (dp[n][k])
	{
		cout << "Yes" << endl;
	}
	else
	{
		cout << "No" << endl;
	}
	return 0;
} 

这样做时间复杂度为O(KΣimi),下面降低时间复杂度,定义状态:
dp[i+1][j]=用前 i 种数加和得到 j 时第种数最多能剩余多少个(不能加和得到的情况下为-1)
递推式:
在这里插入图片描述

#include <iostream>
#include <cstring>
using namespace std;
int a[105], m[105];
int dp[105];
int main()
{
	memset(dp, -1, sizeof(dp));
	int n, k;
	cin >> n >> k;
	for (int i = 0; i < n; i++)
	{
		cin >> a[i] >> m[i];
	}
	dp[0] = 0;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j <= k; j++)
		{
			if (dp[j] >= 0)
			{
				dp[j] = m[i];
			}
			else if (j < a[i] || dp[j - a[i]] <= 0)
			{
				dp[j] = -1;
			}
			else
			{
				dp[j] = dp[j - a[i]] - 1;
			}
		}
	}
	if (dp[k] >= 0)
	{
		cout << "Yes" << endl;
	}
	else
	{
		cout << "No" << endl;
	}
	return 0;
} 
  1. 最长上升子序列问题
    有一个长为n的数列a0,a1,……a-1。请求出这个序列中最长的上升子序列的长度。上升子序列指的是对于任意的j都满足ai<aj的子序列。
    分析:定义dp[i]=以a为末尾的最长上升子序列的长度,则递推公式如下:
    在这里插入图片描述
#include <iostream>
using namespace std;
/*
5
4 2 3 1 5
*/
int main()
{
	int n;
	cin >> n;
	int a[50], dp[50] = { 0 };
	for (int i = 0; i < n; i++)
	{
		cin >> a[i];
	}
	int res = 0;
	for (int i = 0; i < n; i++)
	{
		dp[i] = 1;
		for (int j = 0; j < i; j++)
		{
			if (a[j] < a[i])
			{
				dp[i] = max(dp[i], dp[j] + 1);
			}
			res = max(res, dp[i]);
		}
	}
	cout << res << endl;
	return 0;
}
  1. 有关记数问题的DP
    有n个无区别的物品,将它们划分成不超过m组,求出划分方法数模M的余数。
    限制条件:1≤m≤n≤1000, 2≤M≤10000
/*
4 3
*/
#include <iostream>
using namespace std;
const int M = 10000;
int main()
{
	int n, m;
	cin >> n >> m;
	int dp[50][50] = { 0 };
	dp[0][0] = 1;
	for (int i = 1; i <= m; i++)
	{
		for (int j = 0; j <= n; j++)
		{
			if (j >= i)
			{
				dp[i][j] = (dp[i - 1][j] + dp[i][j - i]) % M;
			}
			else
			{
				dp[i][j] = dp[i - 1][j];
			}
		}
	}
	cout << dp[m][n] << endl;
	return 0;
}
  1. 多重集组合数
    有n种物品,第i种物品有a个。不同种类的物品可以互相区分但相同种类的无法区分。从这些物品中取出m个的话,有多少种取法?求出方案数模M的余数。
    A限制条件:1≤n≤1000,1≤m≤1000,1≤a≤1000,2≤M≤10000
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值