无后效性,当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态,没有关系。。
最优子结构,问题的最优解,可以由相互关联的子问题的最优解推出来。
重叠子问题,后续问题的求解,需要之前的数据。
线性动态规划
最长单调递增子序列
题意:对于给定的一个序列(a1, a2, …, aN),我们可以得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8).
输入:第一行n,表示序列长度。第二行n个数,以空格隔开。
最优子结构:dp[4]的值要由dp[1],dp[2],dp[3]中的最大值确定且a[4]>a[i],i=1…3。
重叠子问题:在最优子结构的说明中已经体现。
无后效性:dp[4]的确定只关心a[4]与a[i]的大小关系,不关心dp[1…3]是如何确定的。
代码:
#include <bits/stdc++.h>
#define eps 1e-15
using namespace std;
int n;
int dp[105],a[105];
void solve(){
dp[1]= 1;
for(int i= 2; i<= n; i++){
int m= 0;
for(int j= 1; j< i; j++)if(a[i]>= a[j] && dp[j]> m)m= dp[j];
dp[i]= m+1;
}
}
int main()
{
cin>>n;
for(int i = 1; i <= n; i++)cin>>a[i];
solve();
int maxx=0;
for(int i= 1; i<= n; i++)if(dp[i]> maxx)maxx= dp[i];
cout<<maxx;
return 0;
}
最大字段和
题意:给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大,如果最大值为负数则最大值为0。
输入:第一行n,表示序列长度。第二行n个数,以空格隔开。
最优子结构:dp[4]的值要由dp[1],dp[2],dp[3]中与a[4]和的最大值确定。
重叠子问题:在最优子结构的说明中已经体现。
无后效性:dp[4]的确定只关心a[4]+a[i]的值,不关心dp[1…3]是如何确定的。
代码:
#include <bits/stdc++.h>
#define eps 1e-15
using namespace std;
int n,a[105];
int dp[105];
//b,l用于输出最优解
int solve(int &b,int &l){
int sum=0;
b=0;l=0;
for(int i=1;i<=n;i++){
if(dp[i-1]==0)b=i;
dp[i]=max(dp[i-1]+a[i],0);
if(dp[i]>sum){sum=dp[i];l=i;}
}
return sum;
}
int main()
{
cin>>n;
int b,l;
for(int i=1;i<=n;i++)cin>>a[i];
int sum=solve(b,l);
cout<<b<<" "<<l<<endl;
cout<<sum;
return 0;
}
最长公共子序列
题意:若给定序列X={x1,x2,…,xm},则另一序列Z={z1,z2,…,zk},是X的子序列是指存在一个严格递增下标序列{i1,i2,…,ik}使得对于所有j=1,2,…,k有:zj=xij。
输入:第一行两个整数,以空格隔开,分别表示两个序列的长度。第二行两串字符串,以空格隔开。
最优子结构:因为每次对两个序列比较时都会比较a[i]和b[j]是否相同
或抄下c[i][j-1]
和c[i-1][j]
的较大值,所以才存在最优子结构,每次比较也只需要关注a[i]和b[j]是否相同
以及c[i][j-1]和c[i-1][j]谁大谁小
。
重叠子问题:在最优子结构的说明中已经体现。
无后效性:应该也可以在最优子结构中体现。
代码:
#include <iostream>
#define MAX 101
using namespace std;
char a[MAX],b[MAX];
int m,n;
int c[MAX][MAX];//c[i][j]表示a序列到i,b序列到j的最长子序列长度
string s[MAX][MAX];
void LCSLength(){
int i, j;
for(i = 0; i <= m; i++)c[i][0]=0;
for(j = 0; j <= n; j++)c[0][j]=0;
for(i = 1; i <= m; i++)
for(j = 1; j <= n; j++){
if(a[i] == b[j]){c[i][j] = c[i-1][j-1] + 1;s[i][j] = s[i-1][j-1] + a[i];}
else if(c[i-1][j] > c[i][j-1]){c[i][j] = c[i-1][j];s[i][j] = s[i-1][j];}
else {c[i][j] = c[i][j-1];s[i][j] = s[i][j-1];}
}
}
int main()
{
cin>>m>>n;
for(int i = 1;i <= m; i++)
cin>>a[i];
for(int i = 1;i <= n; i++)
cin>>b[i];
LCSLength();
for(int i=1; i<=m; i++){
for(int j=1; j<=n; j++)
cout<<c[i][j];
cout<<endl;
}
cout<<c[m][n]<<" ";
cout<<s[m][n];
return 0;
}
合唱队形
题意:N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1,2…,K,他们的身高分别为T1,T2,…,TK, 则他们的身高满足T1<…Ti+1>…>TK(1<=i<=K)。已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
输入:第一行n,表示n位同学。第二行n个数,表示每个同学的身高。
只需要运用两次最长单调子序列
代码:
#include <bits/stdc++.h>
#define eps 1e-15
using namespace std;
int n;
int a[102];
int dp[102];
int dpf(int i,int k){
dp[i]= 1;
for(int l= i+1; l<= k; l++){
int m= 0;
for(int j= i; j< l; j++)if(a[l]> a[j] && dp[j]> m)m= dp[j];
dp[l]= m+1;
}
int maxx=0;
for(int l= i; l<= k; l++)if(dp[l]> maxx)maxx= dp[l];
return maxx;
}
int solve(){
int ans=0,num;
for(int k=1;k<n;k++){
if(a[k]<a[k+1]){
num = dpf(1,k) + dpf(k+1,n);
if(num > ans){ans = num;/*cout<<k<<" "<<ans<<endl;*/}
}
}
return ans;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
cout<<n-solve();
return 0;
}
区间动态规划
矩阵连乘积问题
题意:给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2…,n-1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。
最优子结构:每次对矩阵进行加括号都要选择使(m[i][k]+m[k+1][j]+p[i-1]*p[i]*p[j])
最小的k,这样一直分最后就变成一个矩阵的情况,然后从1个矩阵到n个矩阵递推。
分析:此问题需要知道每几个矩阵乘积的值,所以需要算出全部的两个矩阵乘积,三个矩阵乘积等等,每一次循环都可以组成一个矩阵对角线
代码:
#include <iostream>
#define MAX 51
using namespace std;
int m[MAX][MAX];
int p[MAX];
int s[MAX][MAX];
/*
m[i][j]=min(m[i][k]+m[k+1][j]+p[i-1]*p[i]*p[j])
*/
void matrixChain(int n){
for(int i = 1; i <= n; i++)m[i][i]=0;
for(int r = 2; r <= n; r++)//表示对角线
for(int i = 1; i <= n-r+1; i++){
int j = r+i-1;
int temp = 99999999;
for(int k = i; k < j; k++){
if(m[i][k] + m[k+1][j] + p[i-1]*p[k/*加的乘积需要注意*/]*p[j] < temp){
m[i][j] = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
temp = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
s[i][j] = k;
}
}
}
}
//找出最优解
void traceBack(int i,int j){
if(i == j){cout<<"A"<<i;return;}
int k = s[i][j];
cout<<"(";
traceBack(i,k);
traceBack(k+1,j);
cout<<")";
}
int main()
{
int n;
cin>>n;
for(int i = 1; i <= n; i++){cin>>p[i-1]>>p[i];}
matrixChain(n);
cout<<m[1][n];
traceBack(1,n);
return 0;
}
背包问题
0-1背包
#include <bits/stdc++.h>
#define eps 1e-15
using namespace std;
int n,value;
int w[105],v[105];
int dp[105][105];
int ans[105];
int max(int a,int b){
return a>b?a:b;
}
void solve(){
for(int i=n;i>=1;i--)
for(int j=1;j<=value;j++){
if(j<w[i])dp[i][j] = dp[i+1][j];
else dp[i][j] = max(dp[i+1][j], dp[i+1][j-w[i]] + v[i]);
}
}
void traceBack(){
int i = 1,j = value;
while(i <= n){
// cout<<i<<" "<<j<<" "<<dp[i][j]<<" "<<dp[i+1][j-w[i]] + v[i]<<endl;
if(dp[i][j] == dp[i+1][j-w[i]] + v[i]){ans[i] = 1; j = j-w[i]; i++;}
else {i++;}
}
}
int main()
{
cin>>n>>value;
for(int i=1;i<=n;i++)cin>>w[i]>>v[i];
solve();
traceBack();
cout<<dp[1][value]<<endl;
for(int i=1;i<=n;i++)
cout<<ans[i]<<" ";
return 0;
}
栈
题意:现在可以进行两种操作。将一个数,从操作数序列的头端移到栈的头端(对应数据结构栈的 push 操作)。将一个数,从栈的头端移到输出序列的尾端(对应数据结构栈的 pop 操作)。你的程序将对给定的 n,计算并输出由操作数序列 1,2,…,n 经过操作可能得到的输出序列的总数。
代码:
#include <bits/stdc++.h>
#define eps 1e-15
using namespace std;
//dp i j,表示栈内有i个,输出了j个情况的个数,这是关键
//背包类型的题目,只有出栈和入栈两种选择,那相应的dp[i][j]表示的意义就也知道了
//卡特兰数 f[i] = f[0]*f[n-1]+f[1]f[n-2]+...+f[n-1]f[0];
int dp[20][20],n;
void solve(){
for(int i = 1; i <= n; i++)dp[i][0] = 1;
for(int i = 1; i <= n; i++)
for(int j = 0; j <= n-i; j++)
if(j == 0)dp[j][i] = dp[j+1][i-1];
else dp[j][i] = dp[j-1][i] + dp[j+1][i-1];
}
int main()
{
cin>>n;
solve();
cout<<dp[0][n];
return 0;
}
多重背包
摆花
题意:小明的花店新开张,为了吸引顾客,他想在花店的门口摆上一排花,共 m 盆。通过调查顾客的喜好,小明列出了顾客最喜欢的 n 种花,从 1 到 n 标号。为了在门口展出更多种花,规定第 i 种花不能超过ai盆,摆花时同一种花放在一起,且不同种类的花需按标号的从小到大的顺序依次摆列。
试编程计算,一共有多少种不同的摆花方案。
代码:
#include <bits/stdc++.h>
#define eps 1e-15
using namespace std;
//n种类,m最大摆数量.dp表示摆到第i种,摆了j盆花的方案数.p表示摆i盆的方案数
int n,m,dp[105][105],a[105];
int p[105];
void solve(){
/*for(int i=2; i<=n; i++){
dp[i][0] = 1;
for(int j=1; j<=m; j++)
for(int k=a[i]>j?j:a[i]; k>=0; k--)
dp[i][j] = (dp[i][j]%1000007+dp[i-1][j-k]%1000007)%1000007;
}*/
//因为每次循环只用到了前一次的方案状态,可以进行数组压缩
for(int i=0; i<=a[1]; i++)
p[i] = 1;
for(int i=2; i<=n; i++)
for(int j=m; j>=0; j--)
for(int k=1; k<=min(a[i],j); k++)
//每次多摆一盆,考到虑第i种花摆完,画一下表格好理解
p[j] = (p[j] + p[j-k])%1000007;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
solve();
/* for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++)
cout<<dp[i][j]<<" ";
cout<<endl;
}
cout<<dp[n][m];*/
cout<<p[m];
/* for(int i=0; i<=m; i++){
cout<<p[i]<<" ";
}*/
return 0;
}
完全背包
疯狂的采药
题意:LiYuxiang 是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是 LiYuxiang,你能完成这个任务吗?
此题和原题的不同点:
-
每种草药可以无限制地疯狂采摘。
-
药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!
输入:
输入第一行有两个整数,分别代表总共能够用来采药的时间 t 和代表山洞里的草药的种类 m。
第 2 到第 (m + 1) 行,每行两个整数,第(i+1) 行的整数 a_i, b_ia 分别表示采摘第 i 种草药的时间和该草药的价值。
#include <bits/stdc++.h>
#define eps 1e-15
using namespace std;
long long int v[10005],w[10005];
long long int dp[10000005];
//dp表示花费i时间的最大价值
void solve( long long int t,long long int m){
for(int i= m; i >= 1; i--)
for(int j = w[i]; j <= t; j++)
{
dp[j] = max(dp[j], dp[j-w[i]]+v[i]);
//会用到此次循环产生的状态,简化了num,
//状态转移方程 dp[j] = max(dp[j], dp[j-num*w[i]]+num*v[i]);
//相当于 dp[i][j] = max(dp[i+1][j], dp[i+1][j-w[i]] + v[i])
//状态转移方程 max { dp[i+1][v−k×w[i]] + k × c[i] } 0≤k×w[i]≤v。
}
}
int main()
{
long long int t,m;cin>>t>>m;
for(int i=1;i<=m;i++)cin>>w[i]>>v[i];
for(int i=0;i<=m+1;i++)
dp[i]=0;
solve(t,m);
cout<<dp[t];
return 0;
}
持续更新,如有不足,还请提出。