动态规划初步
动态规划(DP)程序设计是求解最优化问题的一种方法,融合了贪心、分治、递推的思想。
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态;或倒过来,从结束状态开始,通过对中间阶段决策的选择,达到初始状态。这些决策形成一个决策序列,同时确定了完成整个过程的一条活动路线,通常是求最优活动路线。
动态规划问题的求解必须满足两个条件:最优化原理和无后效性原则。
最优化原理:
指子问题的局部最优将导致整个问题的全局最优,即一个问题的最优解只取决于其子问题的最优解,可分治。
无后效性原则:
某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响,即计算过程中的任何一步,都是对前面的总结,方便递推。
下面来看几个常见的动态规划模型:
1.背包问题
[NOIP2005 普及组] 采药
题目描述
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入格式
第一行有 2 2 2 个整数 T T T( 1 ≤ T ≤ 1000 1 \le T \le 1000 1≤T≤1000)和 M M M( 1 ≤ M ≤ 100 1 \le M \le 100 1≤M≤100),用一个空格隔开, T T T 代表总共能够用来采药的时间, M M M 代表山洞里的草药的数目。
接下来的 M M M 行每行包括两个在 1 1 1 到 100 100 100 之间(包括 1 1 1 和 100 100 100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式
输出在规定的时间内可以采到的草药的最大总价值。
样例 #1
样例输入 #1
70 3
71 100
69 1
1 2
样例输出 #1
3
提示
【数据范围】
- 对于 30 % 30\% 30% 的数据, M ≤ 10 M \le 10 M≤10;
- 对于全部的数据, M ≤ 100 M \le 100 M≤100。
【题目来源】
NOIP 2005 普及组第三题
代码
1.枚举:复杂度太高;
2.动态规划:用f[j]表示重量不超过j的方案中,能获得的最大价值。注意第2层循环中必须逆向,因为每一种都只有一个。
#include <bits/stdc++.h>
using namespace std;
int T,m;
int p[105],t[105],f[1005];
int main()
{
cin>>T>>m;
for(int i=1;i<=m;i++)
cin>>t[i]>>p[i];
for(int i=1;i<=m;i++)
for(int j=T;j>=t[i];j--)
f[j]=max(f[j],f[j-t[i]]+p[i]);
cout<<f[T];
return 0;
}
2.最长上升子序列
最长上升子序列
题目描述
这是一个简单的动规板子题。
给出一个由 n ( n ≤ 5000 ) n(n\le 5000) n(n≤5000) 个不超过 1 0 6 10^6 106 的正整数组成的序列。请输出这个序列的最长上升子序列的长度。
最长上升子序列是指,从原序列中按顺序取出一些数字排在一起,这些数字是逐渐增大的。
输入格式
第一行,一个整数 n n n,表示序列长度。
第二行有 n n n 个整数,表示这个序列。
输出格式
一个整数表示答案。
样例 #1
样例输入 #1
6
1 2 4 1 3 4
样例输出 #1
4
提示
分别取出 1 1 1、 2 2 2、 3 3 3、 4 4 4 即可。
代码
f[i]表示以a[i]结尾的最长上升子序列的长度。状态转移时须同时满足最长和上升两个条件。
下面解法的复杂度还可以降低。
#include <bits/stdc++.h>
using namespace std;
int a[5005],f[5005];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
f[1]=1;
int m=1;
for(int i=2;i<=n;i++)
{
f[i]=1;
for(int j=1;j<i;j++)
{
if(a[j]<a[i]&&f[j]>f[i]-1)
f[i]=f[j]+1;
}
if(f[i]>m)
m=f[i];
}
cout<<m;
return 0;
}
3.最大子段和
最大子段和
题目描述
给出一个长度为 n n n 的序列 a a a,选出其中连续且非空的一段使得这段和最大。
输入格式
第一行是一个整数,表示序列的长度 n n n。
第二行有 n n n 个整数,第 i i i 个整数表示序列的第 i i i 个数字 a i a_i ai。
输出格式
输出一行一个整数表示答案。
样例 #1
样例输入 #1
7
2 -4 3 -1 2 -4 3
样例输出 #1
4
提示
样例 1 解释
选取 [ 3 , 5 ] [3, 5] [3,5] 子段 { 3 , − 1 , 2 } \{3, -1, 2\} {3,−1,2},其和为 4 4 4。
数据规模与约定
- 对于 40 % 40\% 40% 的数据,保证 n ≤ 2 × 1 0 3 n \leq 2 \times 10^3 n≤2×103。
- 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 2 × 1 0 5 1 \leq n \leq 2 \times 10^5 1≤n≤2×105, − 1 0 4 ≤ a i ≤ 1 0 4 -10^4 \leq a_i \leq 10^4 −104≤ai≤104。
代码
这里提供一种新的想法。首先要想清楚,如何确定出现了一个比当前连续子段和更大的连续子段?
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n,s=0,m=0,a;
cin>>n;
cin>>a;
s=a,m=a;
n-=1;
while(n--)
{
cin>>a;
s+=a;
if(s>m) m=s;
else if(s<0) s=0;
}
cout<<m;
return 0;
}
4.最长公共子序列
LCS
题面翻译
题目描述:
给定一个字符串 s s s 和一个字符串 t t t ,输出 s s s 和 t t t 的最长公共子序列。
输入格式:
两行,第一行输入 s s s ,第二行输入 t t t 。
输出格式:
输出 s s s 和 t t t 的最长公共子序列。如果有多种答案,输出任何一个都可以。
说明/提示:
数据保证 s s s 和 t t t 仅含英文小写字母,并且 s s s 和 t t t 的长度小于等于3000。
题目描述
文字列 $ s $ および $ t $ が与えられます。 $ s $ の部分列かつ $ t $ の部分列であるような文字列のうち、最長のものをひとつ求めてください。
输入格式
入力は以下の形式で標準入力から与えられる。
$ s $ $ t $
输出格式
$ s $ の部分列かつ $ t $ の部分列であるような文字列のうち、最長のものをひとつ出力せよ。 答えが複数ある場合、どれを出力してもよい。
样例 #1
样例输入 #1
axyb
abyxb
样例输出 #1
axb
样例 #2
样例输入 #2
aa
xayaz
样例输出 #2
aa
样例 #3
样例输入 #3
a
z
样例输出 #3
样例 #4
样例输入 #4
abracadabra
avadakedavra
样例输出 #4
aaadara
提示
注釈
文字列 $ x $ の部分列とは、$ x $ から $ 0 $ 個以上の文字を取り除いた後、残りの文字を元の順序で連結して得られる文字列のことです。
制約
- $ s $ および $ t $ は英小文字からなる文字列である。
- $ 1\ \leq\ |s|,\ |t|\ \leq\ 3000 $
Sample Explanation 1
答えは axb
または ayb
です。 どちらを出力しても正解となります。
Sample Explanation 3
答えは `` (空文字列) です。
代码
依然使用DP思想。
输出最长公共子序列时需要回溯,是与开头找最长公共子序列长度相反的过程。
#include <bits/stdc++.h>
using namespace std;
int f[3005][3005];
char ans[3005];
int main()
{
string s1,s2;
int i,j,n,m;
cin>>s1>>s2;
n=s1.length();
m=s2.length();
for(i=0;i<=n;i++) f[i][0]=0;
for(j=0;j<=m;j++) f[0][j]=0;
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
if(s1[i-1]==s2[j-1]) f[i][j]=f[i-1][j-1]+1;
else f[i][j]=max(f[i-1][j],f[i][j-1]);
while(f[n][m]>0)
{
if(s1[n-1]==s2[m-1])
{
ans[f[n][m]]=s1[n-1];
n--,m--;
}
else
{
if(f[n][m]==f[n-1][m]) n--;
else m--;
}
}
for(int i=1;;i++)
{
if(ans[i]!='\0')
cout<<ans[i];
else break;
}
return 0;
}