N阶楼梯上楼问题
题意: 一次走两阶或者一阶,问有多少种上楼方式。
题目网址
- 定义: dp[i]为i阶楼梯方式。目标:dp[n]。
- 考察最后一步,要么上一阶,要么上二阶。故状态转移方程为dp[n] = dp[n-1] + dp[n-2]
- 初态dp[1] = 1,dp[2] = 2.
代码:
#include <stdio.h>
const int max_n = 100;
int dp[max_n];
int main(){
dp[1] = 1;
dp[2] = 2;
for(int i=3; i<max_n; i++)
dp[i] = dp[i-1] + dp[i-2];
int n;
while(scanf("%d", &n)!=EOF)
printf("%d\n", dp[n]);
return 0;
}
不容易系列之一
题意: 完全错排问题
题目网址
- dp[i]表示1到i完全错排种类数。目标dp[n].
- 考察第一个数与其他数。从后面选择一个数(第k个)占第一个的位。此时有两种情况。(1)第一个数占第k个数的位。这时有dp[n-2]种。(2)第一个数不能占第k个位,此时第一个数在错排中可以等价为第k个数,这时有dp[n-1]种。
- 由于又n-1种选择。故错排的状态转移方程为dp[n] = (n-1) * (dp[n-1] + dp[n-2])
- 初态dp[2] = 1,dp[3] = 2
代码:
#include <stdio.h>
const int max_n = 22;
typedef long long LL; //使用long long, 不然溢出
LL dp[max_n];
int main(){
dp[2] = 1;
dp[3] = 2;
for(int i=4; i<max_n; i++)
dp[i] = (i-1) * (dp[i-1] + dp[i-2]);
int n;
while(scanf("%d", &n)!=EOF)
printf("%lld\n", dp[n]); //lld输出long long
return 0;
}
吃糖果
题意: 可抽象为上楼梯问题
题目网址
- dp[i]表示i块巧克力的吃法数量
- 考察最后一天吃。要么吃1块,要么吃两块
- 状态转移方程同上楼梯。
代码:
#include <stdio.h>
const int max_n = 22;
typedef long long LL; //使用long long, 不然溢出
LL dp[max_n];
int main(){
dp[2] = 1;
dp[3] = 2;
for(int i=4; i<max_n; i++)
dp[i] = (i-1) * (dp[i-1] + dp[i-2]);
int n;
while(scanf("%d", &n)!=EOF)
printf("%lld\n", dp[n]); //lld输出long long
return 0;
}
拦截导弹
题意: 求最长不递增子序列长度
题目网址
- 定义dp[i]:为以第i个数为结尾的最长不递增子序列长度. 目标求: max{dp[i] | i from 1 to n}
- 考察第i个数,要么自立门户,要么跟着前i-1个后面任意一个。
- 转移方程 dp[i] = max{1, dp[j]+1 | j<i && a[j] >= a[i]}
- 初态 dp[1] = 1
代码:
#include <stdio.h>
#include <iostream> // 此两句为max函数所需
using namespace std;
const int max_n = 27;
const int inf = 0x7fffffff;
int dp[max_n];
int a[max_n];
void init(){
for(int i=0; i<max_n; i++)
dp[i] = 1;
}
int main(){
int n;
while(scanf("%d", &n)!= EOF){
for(int i=1; i<=n; i++)
scanf("%d", &a[i]);
init();
for(int i=2; i<=n; i++){
for(int j=1; j<i; j++){
if(a[j] >= a[i]){
dp[i] = max(dp[i], dp[j]+1);
}
}
}
int ans = -inf;
for(int i=1; i<=n; i++)
ans = max(ans, dp[i]);
printf("%d\n", ans);
}
return 0;
}
合唱队形
题意:合唱队形要成为身高先增后减。问最少踢多少人。
题目网址
- 定义:dp1[i],从左到右最长递增子序列。dp2[i],从右到左最长递增子序列。求出两个dp的值
- 对每一个人,假设其在最中间。利用两个dp数组求踢的人。
- 答案即为最大值.
代码:
#include <stdio.h>
#include <iostream>
using namespace std;
const int max_n = 105;
const int inf = 0x7fffffff;
int dp1[max_n], dp2[max_n], a[max_n];
void init(){
for(int i=0; i<max_n; i++)
dp1[i] = dp2[i] = 1;
}
int main(){
int n;
while(scanf("%d", &n)!=EOF){
for(int i=1; i<=n; i++)
scanf("%d", &a[i]);
init();
for(int i=2; i<=n; i++){
for(int j=1; j<i; j++){
if(a[j] < a[i])
dp1[i] = max(dp1[i], dp1[j] + 1);
}
}
for(int i=n-1; i>=1; i--){
for(int j=n; j>i; j--){
if(a[j] < a[i])
dp2[i] = max(dp2[i], dp2[j] + 1);
}
}
int ans = inf;
for(int i=1; i<=n; i++)
ans = min(ans, n-dp1[i]-dp2[i]+1);
printf("%d\n", ans);
}
return 0;
}
Coincidence
题意:求两个字符串的最长公共子序列长度
题目网址
- dp[i][j]:s1前i个字符,s2前j个字符的最长公共子序列长度。
- 状态转移。若s[i-1] == s[j-1],dp[i][j] = dp[i-1][j-1] + 1。否则dp[i][j] = max(dp[i-1][j], dp[i][j-1])
- 初态dp[0][j] = 0, dp[i][0] = 0.
- 注意:字符串和dp的下标问题。
代码:
#include <iostream>
using namespace std;
const int max_n = 104;
const int inf = 0x7fffffff;
int dp[max_n][max_n];
void init(){
for(int i=0; i<max_n; i++){
dp[i][0] = dp[0][i] = 0;
}
}
int main(){
string s1, s2;
int length1, length2;
while(cin >> s1 >> s2){
init();
length1 = s1.length();
length2 = s2.length();
for(int i=1; i<=length1; i++){
for(int j=1; j<=length2; j++){
if(s1[i-1] == s2[j-1])
dp[i][j] = dp[i-1][j-1] + 1;
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
cout << dp[length1][length2] << endl;
}
return 0;
}
搬寝室
题意:问一组数取k对两两组合,求他们的平方和的最小值
题目网址
有n件物品,要搬走k对,每一对花费(a-b) * (a-b)体力,问所花最少体力。
- 根据推导得到结论:最优方案种,成对的重量是两两相邻的。
- 定义dp[i][j]:前i件物品里搬j对的最少体力。目标dp[n][k]
- 对第i件物品:若不选,则dp[i][j] = dp[i-1][j].若选,则必与第j-1件物品配对,故dp[i][j] = dp[i-2][j] + (a[j] - a[j-1]) * (a[j] - a[j-1]),选最小的一个.
- 初态dp[i][0] = 0 (选0对需要0体力), 由状态转移方程,需要用到i-2的条件,故还需初始化dp[1][j], 而dp[1][j] (j>=1) = inf (不可能达到的状态,一件物品选大于一对物品)
代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int max_n = 2000 + 5;
typedef long long LL;
const LL inf = 0x7fffffffffffffff;
LL dp[max_n][max_n/2];
int init(){
for(int i=0; i<max_n; i++){
for(int j=0; j<max_n/2; j++)
dp[i][j] = inf;
dp[i][0] = 0;
}
}
int main(){
int n, k;
LL a[max_n];
while(cin >> n >> k){
for(int i=1; i<=n; i++)
cin >> a[i];
init();
sort(a+1, a+n+1);
LL tmp1, tmp2;
for(int i=2; i<=n; i++){
for(int j=1; j<=k; j++){
tmp1 = tmp2 = inf;
if(dp[i-1][j] != inf) //由上一个状态转移,上一个状态必须是可达的
tmp1 = dp[i-1][j];
if(dp[i-2][j-1] != inf)
tmp2 = dp[i-2][j-1] + (a[i]-a[i-1])*(a[i]-a[i-1]);
dp[i][j] = min(tmp1, tmp2);
}
}
cout << dp[n][k] << endl;
}
return 0;
}
Greedy Tino
题意: 从一组数中,选出两个集合,两个集合的和相等。问其中一个集合的和的最大值是多少。
题目网址
- 定义dp[i][j+offset]:前i件物品中左集合和比右集合和大j,两个集合数的总和。目标:dp[n][0+offset] / 2。
- 对第i件物品,我们可以选择不要、放左集合、放右集合三种情况。
- 状态转移方程:dp[i][j+offset] = max(dp[i-1][offset], dp[i-1][j+offset+a[i]] + a[i], dp[i-1][j+offset-a[i]+a[i]).
- 初始状态dp[0][j], j不为0 = -inf(不可达)
- 也要考察有值为零的数的情况。
代码:
#include <iostream>
using namespace std;
const int max_n = 105;
const int max_w = 4005;
const int offset = 2000;
const int inf = 0x7ffffff;
int dp[max_n][max_w];
void init(){
for(int i=-2000; i<=2000; i++)
dp[0][i+offset] = -inf;
dp[0][0+offset] = 0;
}
int main(){
int T,n,tmp1, tmp2, tmp3;
int a[max_n];
int havezero;
cin >> T;
for(int t=1; t<=T; t++){
cin >> n;
init();
havezero = 0;
for(int i=1; i<=n; i++){
cin >> a[i];
if(a[i] == 0)
havezero = 1;
}
for(int i=1; i<=n; i++){
for(int j=-2000; j<=2000;j++){
tmp1 = tmp2 = tmp3 = -inf;
// 不要
if(dp[i-1][j+offset] != -inf)
tmp1 = dp[i-1][j+offset];
// 放左边, 注意这个边界条件!!!!
if(j+a[i] <= 2000 && dp[i-1][j+a[i]+offset] !=- inf)
tmp2 = dp[i-1][j+a[i]+offset] + a[i];
// 放右边
if(j-a[i] >= -2000 && dp[i-1][j-a[i]+offset]!= -inf)
tmp3 = dp[i-1][j-a[i]+offset] + a[i];
dp[i][j+offset] = max(tmp1, max(tmp2, tmp3));
}
}
cout << "Case " << t << ": ";
if(dp[n][offset] == 0){
if(!havezero)
cout << -1 << endl;
else
cout << 0 << endl;
}
else
cout << dp[n][offset] / 2<< endl;
}
return 0;
}
采药
题目大意:典型的0-1背包问题
题目链接
- dp[i][j] 前i个物品,在背包容量j时有的最大价值。 目标dp[n][w]
- 0-1背包问题,对第i件物品,要么选要么不选
- 转态转移:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i] + v[i])
- 可以压缩dp[j] = max(dp[j], dp[j-w[i]] + v[i]), 为反序。
- 初始化,全部初始为0(在0件物品时,无论背包多大都是0)
代码:
#include <iostream>
using namespace std;
const int max_n = 105;
const int max_t = 1005;
int dp[max_t];
int p[max_n], t[max_n];
void init(){
for(int i=0; i<max_t; i++)
dp[i] = 0;
}
int main(){
int T, N;
while(cin >> T >> N){
init();
for(int i=1; i<=N; i++)
cin >> t[i] >> p[i];
for(int i=1; i<=N; i++){
for(int j=T;j>=t[i]; j--) //逆序
dp[j] = max(dp[j], dp[j-t[i]]+ p[i]);
}
cout << dp[T] << endl;
}
return 0;
}
Piggy-Bank
题目大意:变种完全背包问题,要刚好装满且最小
题目链接
- dp[i][j]:前i件物品,重量为j时,刚好转满,物品最小价值。目标dp[n][w].
- 对第i件物品,可以选0件,1件,…, k件。
- 状态转移:注意0件有dp[i-1][j]转移而来,k>=1的最优值由dp[i][j-w[i]]转移而来。故dp[i][j] = min(dp[i-1][j], dp[i][j-W[i] + P[i])
- 同样可以压缩。且为正序
- 初态。dp[0] = 0, dp[j] = inf (不可达, j不等于0, 物品为0时不可能装满容量不为0的背包)。
代码:
#include <iostream>
using namespace std;
const int max_n = 505;
const int max_w = 10005;
const int inf = 0x7fffffff;
int P[max_n], W[max_n];
int dp[max_w];
void init(){
for(int i=1; i<max_w; i++)
dp[i] = inf;
dp[0] = 0;
}
int main(){
int T;
cin >> T;
while(T--){
int E, F, C;
cin >> E >> F;
C = F-E;
int N;
cin >> N;
for(int i=1; i<=N; i++)
cin >> P[i] >> W[i];
int tmp1, tmp2;
init();
for(int i=1; i<=N; i++){
for(int j=W[i]; j<=C; j++){ //核心,这里正序.
tmp1 = tmp2 = inf; //对于是否能够转移的问题,最好是写出tmp,加上判断
if(dp[j] != inf)
tmp1 = dp[j];
if(dp[j-W[i]] != inf)
tmp2 = dp[j-W[i]] + P[i];
dp[j] = min(tmp1, tmp2);
}
}
if(dp[C] == inf)
cout << "This is impossible." << endl;
else
cout << "The minimum amount of money in the piggy-bank is " << dp[C] << "." << endl;
}
return 0;
}
珍惜现在,感恩生活
题目大意: 典型的多重背包问题.
题目链接
- 使用2进制分解将多重背包分解为0-1背包求解.
代码:
#include<iostream>
using namespace std;
const int max_n = 105;
const int max_w = 105;
int dp[max_w];
void init(){
for(int i=0; i<max_w; i++)
dp[i] = 0;
}
int main(){
int C;
cin >> C;
int W[max_n * 20], V[max_n * 20];
while(C--){
init();
int w,n;
cin >> w >> n;
int p, h, c;
int cnt = 1;
for(int i=1; i<=n; i++){
cin >> p >> h >> c;
// -------------------------------------------
int t = 1;
do{
W[cnt] = t * p;
V[cnt++] = t * h;
c -= t;
t *= 2;
}while(c - t > 0);
W[cnt] = c * p;
V[cnt++] = c * h;
}
// ---------------------------------------------
for(int i=1; i<=cnt-1; i++){
for(int j=w; j>=W[i]; j--)
dp[j] = max(dp[j],dp[j-W[i]] + V[i]);
}
cout << dp[w] << endl;
}
return 0;
}
总结
通过本章,我基本了解了动态规划中的典型问题。总的来说,动态规划关键是状态的定义,然后根据物理意义去得到状态转移方程,一定要考虑所有的转移情况。最后确定初态。感觉有点像数学归纳法的味道。
知识点
- 斐波拉契数列的场景
- 错排公式
- 最长上升子序列
- 最长公共子序列
- 天平类问题
- 0-1背包
- 完全背包
- 多重背包