01背包
相关知识链接acm之旅–动态规划(DP)中的0-1背包问题
复杂度O(nW)。n为种类数目,W为背包容量。
01背包2
当W太大,且v比较小时。
代码:
完全背包问题
参考博文:acm之旅–动态规划(DP)背包问题
最长公共子序列
参考博文:acm之旅–动态规划(DP)最长公共子序列。
最长上升子序列
参考博文:acm之旅–动态规划(DP)最长递增子序列。
多重部分和问题
- 思路:一类典型的动态规划。难点在与状态的构造,不同的状态最终双方复杂度也不一样。
- 方法一:
- 状态的构造:d[i][j]表示使用前i种数字是否能加和成j(bool类型)。
- 终态:d[n][K]。
- 状态转移方程:用前i种数字加和成j等价于,只要用前i-1种数字加和成j、j-ai、j-mixai等中的一种就成立。
d[i][j] = d[i][j] | d[i-1][j-kxai](0<=k<=mi且kxai<=j时) - 初始化:d[][] = false; d[0][0] = true;
- 复杂度:O(nK ∑ 1 n m i \sum_1^nm_i ∑1nmi)
- DP存取bool结果浪费了许多信心。
- 方法二:
- 状态的构造:d[i][j]表示用前i种数加和得到j时第i种数最多能剩余多少个(不能加和得到i的情况下为-1)
- 判断d[n][K]>=0
- 状态转移方程:如果前i-1种数可以得到j时,第i个数就可以留下
m
i
m_i
mi个。如果前i种数加和出 j-
a
i
a_i
ai时第i种数还剩下k(k>0)个,那么d[i][j] = k-1。如果k<0则表示该数字无法达到。
d [ i ] [ j ] = { m i , d [ i − 1 ] [ j ] > = 0 − 1 , j < a i 或 者 d [ i ] [ j − a i ] < = 0 d [ i ] [ j − a i ] − 1 , e l s e d[i][j] = \begin{cases} m_i & & ,d[i-1][j]>=0 \\ -1 & & ,j<a_i 或者 d[i][j-a_i]<=0 \\ d[i][j-a_i]-1 & & ,else \end{cases} d[i][j]=⎩⎪⎨⎪⎧mi−1d[i][j−ai]−1,d[i−1][j]>=0,j<ai或者d[i][j−ai]<=0,else - 初始化:dp[][] = -1;d[0][0] = 0,
- 复杂度:O(nK)
- 可以将二维数组一维化。
- 方法一:
代码:
方法一:
#include <iostream>
#include <cstring>
using namespace std;
const int MAX = 10000;
int n, a[MAX], m[MAX], K;
int dp[MAX][MAX];
void Init()
{
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
}
void solve1()
{
for(int i=1; i<=n; i++)
{
for(int j=0; j<=K; j++)
{
for(int k=0; k<=m[i] && k*a[i]<=j; k++)
{
dp[i][j] = dp[i][j] | dp[i-1][j-k*a[i]];
}
}
}
if(dp[n][K]) cout << "Yes" << endl;
else cout << "No" << endl;
}
int main()
{
while(cin >> n)
{
for(int i=1; i<=n; i++)
cin >> a[i];
for(int i=1; i<=n; i++)
cin >> m[i];
cin >> K;
Init();
solve1();
}
return 0;
}
方法二:
int dp[MAX];
void solve()
{
memset(dp, -1, sizeof(dp));
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) printf("Yes\n");
else printf("No\n");
划分数
参考博文:挑战程序设计竞赛:划分数滚动数组
- 题目大意:将数字n划分为m份数字,使得m个数字之和等于n,求划分的方法数。
- 思路:一类典型的动态规划题。难点在于状态转移方程的确定,注意可以使得划分的数字为0。
- 状态构造:dp[i][j]表示j的i划分的方法总数。
- 终态:dp[m][n],表示要求得的n的m划分数。
- 状态转移方程:使用两个条件来递推。1)
当所有划分的数字均大于0时
,n的m划分方法数实际上是等于n-m的m划分,即将n的m划分的数字均减去一。2)当存在划分的数字为0的情况时
,n的m划分实际上等于n的m-1划分。
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] j < i d p [ i − 1 ] [ j ] + d p [ i ] [ j − i ] j > = i dp[i][j] = \begin{cases} dp[i-1][j] & & j<i \\ dp[i-1][j]+dp[i][j-i] & & j>=i \end{cases} dp[i][j]={dp[i−1][j]dp[i−1][j]+dp[i][j−i]j<ij>=i - 初始化:dp[0][*] = 0, dp[0][0] = 1;
代码:
#include <iostream>
#include <cstring>
using namespace std;
const int MAX = 1001;
int m, n, M;
int dp[MAX][MAX];
int main()
{
while(cin >> n >> m)
{
cin >> M;
memset(dp, 0, sizeof(dp));
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;
}
多重集组合数
参考博文:挑战程序设计竞赛:多重组合数
- 题目大意:n种物品,第i种物品有ai个。从这些物品中取出m个,求其取法个数。
- 思路:动态规划问题。重点在与复杂度的降低,两种思考。
- 状态构造:dp[i][j]表示在前i中物品中可以取出j个的取法个数。
- 终态:dp[n][m]。
- 复杂度高(O(n*m^2)):dp[i][j]等价于在第i种中取出[0,min(ai,j)]个物品,其余的从前i-1中取。
- 复杂度低(O(n*m)):dp[i][j]等价于两种情况:1)从第i种不取物品(dp[i-1][j])。2)从第i种中取至少1个物品。即dp[i][j-1]-dp[i-1][j-a[i]-1]。具体情况参考博文。
- 初始化:dp[*][0] = 1。一个都不取的方法为1种。
代码:
类似题目:Ant Counting
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long LL;
const int MAX = 100001;
const int MOD = 1000000;
int dp[1001][MAX];
int a[MAX];
int T, A, S, B;
void Init()
{
for(int i=0; i<=1001; i++)
dp[i][0] = 1;
}
void solve(int n)
{
for(int i=1; i<=T; i++)
{
for(int j=0; j<=n; j++)
{
if(j-1-a[i]>=0)//取模时要避免减法运算结果出现负数
dp[i][j] = (dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1-a[i]]+MOD)%MOD;
else
dp[i][j] = (dp[i-1][j]+dp[i][j-1])%MOD;
}
}
}
int main()
{
int t;
while(scanf("%d%d%d%d", &T, &A, &S, &B)!=EOF)
{
memset(a, 0, sizeof(a));
for(int i=0; i<A; i++)
{
scanf("%d", &t);
a[t]++;
}
Init();
LL cnt = 0;
solve(B);
for(int i=S; i<=B; i++)
cnt = (cnt+dp[T][i])%MOD;
printf("%d\n", cnt);
}
return 0;
}