线性dp体现为在线性空间上的递推,dp的阶段沿着各个维度线性增长,譬如说最长上升子序列,最长公共子序列就是最为简单的两个问题。
这里记录一些《算法竞赛进阶指南》中的线性dp例题
1、[POJ2279]Mr.Young’s Picture Permutations
2、[CH5101]最长公共上升子序列(LCIS)
3、[POJ3666]Making the Grade
4、[CH5102]Mobile Service
5、[CH5103]传纸条(NOIP2008)
6、[CH5104]I-County
7、[CH5105]Cookies
1、[POJ2279]Mr.Young’s Picture Permutations
http://poj.org/problem?id=2279
由于行数k很小,所以我们可以用k个维度表示每一行的状态,设f[a1,a2,…,ak]表示每一行已经安排的学生人数,那么当每次要处理一个新人的时候我们需要考虑所有满足条件的行
:
1、
(可以加入一个新人而不超过限制)
2、
或者
(保证从前往后身高递减)
然后就可以转移了。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
int k;
int n[6];
int main()
{
while(scanf("%d",&k)&&k)
{
memset(n,0,sizeof(n));
for(int i=1;i<=k;i++) scanf("%d",&n[i]);
long long f[n[1]+1][n[2]+1][n[3]+1][n[4]+1][n[5]+1];
memset(f,0,sizeof(f)); f[0][0][0][0][0]=1;
for(int a1=0;a1<=n[1];a1++)
for(int a2=0;a2<=n[2];a2++)
for(int a3=0;a3<=n[3];a3++)
for(int a4=0;a4<=n[4];a4++)
for(int a5=0;a5<=n[5];a5++)
{
if(a1<n[1]) f[a1+1][a2][a3][a4][a5]+=f[a1][a2][a3][a4][a5];
if(a2<n[2] && a1>a2) f[a1][a2+1][a3][a4][a5]+=f[a1][a2][a3][a4][a5];
if(a3<n[3] && a1>a3 && a2>a3) f[a1][a2][a3+1][a4][a5]+=f[a1][a2][a3][a4][a5];
if(a4<n[4] && a1>a4 && a2>a4 && a3>a4) f[a1][a2][a3][a4+1][a5]+=f[a1][a2][a3][a4][a5];
if(a5<n[5] && a1>a5 && a2>a5 && a3>a5 && a4>a5) f[a1][a2][a3][a4][a5+1]+=f[a1][a2][a3][a4][a5];
}
printf("%lld\n",f[n[1]][n[2]][n[3]][n[4]][n[5]]);
}
return 0;
}
2、[CH5101]最长公共上升子序列(LCIS)
http://contest-hunter.org:83/contest/0x50「动态规划」例题/5101 LCIS
我们在之前求解
最长上升子序列(LIS)问题:
设
为以
为结尾的最长上升子序列长度,则
最长公共子序列(LCS)问题:
设
表示
和
的最长公共子序列的长度,则
那么我们把两者结合起来:
设
表示
和
的最长公共上升子序列的长度,且我们默认以
结尾,那么:
我们发现这个转移所需要的复杂度是 级别的,我们需要把它优化到 级别。
因为j每一次增加1,都会带来 的集合增加1,也就是说 的集合的增加是随着j的增加单调的,那么我们就可以维护当前集合中最大的 来减少一维 。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
typedef long long ll;
const int N=3e3+10;
int f[N][N],n;
int a[N],b[N];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) scanf("%d",&b[i]);
f[0][0]=1; int maxx;
for(int i=1;i<=n;i++)
{
maxx=0;
//发现f[i-1][k]的集合的增加是随着j的增加单调的,那么我们就可以维护最大的f[i-1][k]来减少一维
for(int j=1;j<=n;j++)
{
if(b[j]==a[i]) f[i][j]=maxx+1;
else f[i][j]=f[i-1][j];
if(b[j]<a[i]) maxx=max(maxx,f[i-1][j]);
}
}
int ans=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
ans=max(ans,f[i][j]);
printf("%d\n",ans);
return 0;
}
3、[POJ3666]Making the Grade
题意:
给定长度为N的序列A,构造一个长度为N的序列B,满足:
1、B非严格单调,即
或
2、最小化
猜结论:一定有一种构造B的方案,使得B中的数都在A中出现过。
(我也不会证明)
设列DP方程:
设
表示完成前i个数的构造,且
时,S的最小值。
我们根据引理可以离散化f的第二维,且这题与上一题相似,
的集合的增加是随着j的增加单调的增加,那么我们也可以
完成转移。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=2010;
int a[N],b[N];
int f[N][N];//f[i][j]表示把完成前i个数的构造且第Bi=j时的最小代价。
int main()
{
int n;scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
b[i]=a[i];
}
sort(b+1,b+n+1);
int m=unique(b+1,b+n+1)-(b+1);
memset(f,63,sizeof(f)); f[0][0]=0;
int ans=f[1][0];
for(int i=1;i<=n;i++)
{
int minn=f[i-1][0];
for(int j=1;j<=m;j++)
{
minn=min(minn,f[i-1][j]);
f[i][j]=minn+abs(a[i]-b[j]);
}
}
for(int i=1;i<=m;i++)
ans=min(ans,f[n][i]);
printf("%d\n",ans);
return 0;
}
4、[CH5102]Mobile Service
http://contest-hunter.org:83/contest/0x50「动态规划」例题/5102 Mobile Service
DP方程很好设列:
设
前i个请求,三人位置之前分别位于x,y,z最小花费
在第i-1请求完成之后,一定有人会到达 ,那么我们在计算第i个请求的时候,只需要计算剩下两个人情况。
那么重设 表示前i个请求,其他人位置之前分别位于x,y最小花费,则
初始状态设f[0][1][2]=0且p[0]=3表示一开始三人的位置
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
typedef long long ll;
const int N=210;
const int Q=1010;
int f[Q][N][N];
int p[Q];
int c[N][N];
/*
f[i][x][y] 前i个请求,其他人位置之前分别位于x,y最小花费
*/
int main()
{
int n,Q;scanf("%d%d",&n,&Q);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%d",&c[i][j]);
for(int i=1;i<=Q;i++) scanf("%d",&p[i]);
memset(f,0x3f,sizeof(f));
int INF=f[0][0][0];
p[0]=3; f[0][1][2]=0;
for(int i=1;i<=Q;i++)
{
for(int x=1;x<=n;x++)
for(int y=1;y<=n;y++)
{
if(x!=p[i] && y!=p[i])
f[i][x][y]=min(f[i][x][y],f[i-1][x][y]+c[p[i-1]][p[i]]);
if(p[i]!=y && p[i]!=p[i-1])
f[i][p[i-1]][y]=min(f[i][p[i-1]][y],f[i-1][x][y]+c[x][p[i]]);
if(p[i]!=x && p[i]!=p[i-1])
f[i][x][p[i-1]]=min(f[i][x][p[i-1]],f[i-1][x][y]+c[y][p[i]]);
}
}
int ans=INF;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
ans=min(ans,f[Q][i][j]);
printf("%d\n",ans);
return 0;
}
5、[CH5103]传纸条(NOIP2008)
http://contest-hunter.org:83/contest/0x50「动态规划」例题/5103 传纸条
直接开四维 表示一条路的末端在点 另一条路的末端在点 取数之和最大是多少。然后直接就可以转移,判断一下两条路走到一个点上的情况即可。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=51;
int a[N][N];
int f[N][N][N][N];
int n,m;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&a[i][j]);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int u=1;u<=n;u++)
for(int v=1;v<=m;v++)
{
int t=0;
if(i>1 && u>1) t=max(t,f[i-1][j][u-1][v]);
if(i>1 && v>1) t=max(t,f[i-1][j][u][v-1]);
if(j>1 && u>1) t=max(t,f[i][j-1][u-1][v]);
if(j>1 && v>1) t=max(t,f[i][j-1][u][v-1]);
if(i==u && j==v) f[i][j][u][v]=t+a[i][j];
else f[i][j][u][v]=t+a[i][j]+a[u][v];
}
printf("%d\n",f[n][m][n][m]);
return 0;
}
6、[CH5104]I-County
http://contest-hunter.org:83/contest/0x50「动态规划」例题/5104 I-country
一个凸联通块转换为连续的若干行,每行的左端点纵坐标先递减,再递增;右端点纵坐标先递增,再递减
那么我们设
表示当前处理的行数i,直到第i行已经选出来的格子数j,当前行的左右端点l,r,以及表示左端点的增减状态x以及右端点的增减状态y(x=0递增,x=1递减,y也是一样)
我们把左端点递减,右端点递增的状态称为扩张;把左端点递增,右端点递减的状态称为收缩。
注意他们不一定一起扩张收缩,也就是说可能左扩右缩。
那么为了保证只完成一次的扩张与收缩且先收缩在再扩张,收缩的状态可能从扩张或者收缩的状态转移过来来,然而扩张只能从扩张的状态转移过来,确定了转移方针我们就可以发现转移很简单,就可以开始愉快的转移啦。
1、左右端点都扩张:
第一种情况为当前的[l,r]区间的数已经有我想要的j个了,那么我们直接从j=0的状态转移过来就成,第二种的话很好理解,p,q枚举被继承状态的l,r,j-(r-l+1)表示前i-1个选了多少个。
2、左端点扩张右端点收缩:
3、左端点收缩右端点扩张
4、左右都收缩
初始状态为
这题还需要记录路径,我们可以开一个和dp数组相当的数组来记录每一步走了哪一个状态,然后递归输出。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
int f[16][226][16][16][2][2];
int a[16][16],s[16][16];
struct path//记录状态路径
{
int i,j,l,r,x,y;
}pa[16][226][16][16][2][2];
int main()
{
int n,m,K; scanf("%d%d%d",&n,&m,&K);
if(K==0){puts("Oil : 0"); return 0;}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
scanf("%d",&a[i][j]);
s[i][j]=s[i][j-1]+a[i][j];
}
memset(f,128,sizeof(f));
for(int i=0;i<=n;i++) f[i][0][0][0][1][0]=0;
int ans=0; path t;
for(int i=1;i<=n;i++)
for(int j=0;j<=K;j++)
for(int l=1;l<=m;l++)
for(int r=l;r<=m;r++)
{
if(r-l+1>j) break;
int sum=s[i][r]-s[i][l-1];
f[i][j][l][r][1][0]=sum;//扩张
int mx=f[i-1][0][0][0][1][0];
pa[i][j][l][r][1][0]=(path){i-1,0,0,0,1,0};
for(int p=l;p<=r;p++)
for(int q=p;q<=r;q++)
{
if( (q-p+1) + (r-l+1) > j) break;
if(mx<f[i-1][j-(r-l+1)][p][q][1][0])
{
mx=f[i-1][j-(r-l+1)][p][q][1][0];
pa[i][j][l][r][1][0]=(path){i-1,j-(r-l+1),p,q,1,0};
}
}
f[i][j][l][r][1][0]+=mx;
f[i][j][l][r][1][1]=sum;
mx=0;//左扩右缩
for(int p=l;p<=r;p++)
for(int q=r;q<=m;q++)
{
if( (q-p+1) + (r-l+1) > j) break;
for(int y=0;y<=1;y++)
if(mx<f[i-1][j-(r-l+1)][p][q][1][y])
{
mx=f[i-1][j-(r-l+1)][p][q][1][y];
pa[i][j][l][r][1][1]=(path){i-1,j-(r-l+1),p,q,1,y};
}
}
f[i][j][l][r][1][1]+=mx;
f[i][j][l][r][0][0]=sum;
mx=0;//左缩右扩
for(int p=1;p<=l;p++)
for(int q=l;q<=r;q++)
{
if( (q-p+1) + (r-l+1) > j) break;
for(int x=0;x<=1;x++)
if(mx<f[i-1][j-(r-l+1)][p][q][x][0])
{
mx=f[i-1][j-(r-l+1)][p][q][x][0];
pa[i][j][l][r][0][0]=(path){i-1,j-(r-l+1),p,q,x,0};
}
}
f[i][j][l][r][0][0]+=mx;
f[i][j][l][r][0][1]=sum;
mx=0;//收缩
for(int p=1;p<=l;p++)
for(int q=r;q<=m;q++)
{
if( (q-p+1) + (r-l+1) > j) break;
for(int x=0;x<=1;x++)
for(int y=0;y<=1;y++)
if(mx<f[i-1][j-(r-l+1)][p][q][x][y])
{
mx=f[i-1][j-(r-l+1)][p][q][x][y];
pa[i][j][l][r][0][1]=(path){i-1,j-(r-l+1),p,q,x,y};
}
}
f[i][j][l][r][0][1]+=mx;
if(j==K)
{
for(int x=0;x<=1;x++)
for(int y=0;y<=1;y++)
{
if(ans<f[i][j][l][r][x][y])
{
ans=f[i][j][l][r][x][y];
t=(path){i,j,l,r,x,y};
}
}
}
}
printf("Oil : %d\n",ans);
for(;t.i;t=pa[t.i][t.j][t.l][t.r][t.x][t.y])
for(int i=t.l;i<=t.r;i++)
printf("%d %d\n",t.i,i);
return 0;
}
7、[CH5105]Cookies
http://contest-hunter.org:83/contest/0x50「动态规划」例题/5105 Cookies
容易想到一个贪心:怨气值大的人应拿的饼干尽量多。
那么如何尽量拿的多呢?用dp来解决。
设
表示前i个人已经发了j块饼干的最小怨气和。
首先我们按怨气值从大到小排序,然后假设我们已经处理了i-1个孩子,我们现在要处理第i个孩子,有两种情况:
1、第i个孩子获得的饼干比第i-1个孩子少,那么此时a[i]=i-1(因为前面有i-1个人获得的比i个多)
2、第i个孩子获得的饼干数与第i-1个孩子相同,那么我们就需要知道前面有多少个和第i-1个孩子获得的饼干数相同(这些孩子的编号一定是连续的)才能计算a[i]。
但是如果之直接转移,维护他们的时间代价太大了。那么怎么办呢?
我们可以将状态转移做一个等价代换:
因为相对大小的不不变性,我们可以把第i个孩子分配的饼干数缩放到1,再考虑前面的孩子获得的饼干数相等。
1、若第i个孩子的饼干数大于1,则等价于分配j-i个饼干给前i个孩子,每人少拿一块饼干,这样的话获得的饼干数大小顺序不变,怨气之和也不变。
2、若第i个孩子的饼干数等于1,那么就枚举i前面有多少孩子也能获得1块饼干
转移方程为:
因为这题的转移不大直观,所以这题记录路径稍微麻烦。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=30+10;
const int M=5000+10;
struct node
{
int id,x;
}a[N],pa[N][M];
bool cmp(node a,node b){return a.x>b.x;}
int f[N][M];
int sum[N];
int main()
{
int n,m;scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int x; scanf("%d",&x);
a[i]=(node){i,x};
}
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++) sum[i]=sum[i-1]+a[i].x;
memset(f,127,sizeof(f)); f[0][0]=0;
for(int i=1;i<=n;i++)
for(int j=i;j<=m;j++)
{
if(f[i][j]>f[i][j-i])//大于1块饼干
{
f[i][j]=f[i][j-i];
pa[i][j]=(node){i,j-i};
}
int s=sum[i];
for(int k=0;k<i;k++)//只有1块饼干
{
s-=a[k].x;
if(f[i][j]>f[k][j-(i-k)]+k*s)
{
f[i][j]=f[k][j-(i-k)]+k*s;
pa[i][j]=(node){k,j-(i-k)};
}
}
}
printf("%d\n",f[n][m]);
int ans[N]; memset(ans,0,sizeof(ans));
node now=(node){n,m};
while(now.id && now.x)
{
node last=pa[now.id][now.x];
if(last.id==now.id)//上一个等于这一个说明这一个大于1块饼干,所有人一齐减了1
for(int i=1;i<=last.id;i++)
ans[a[i].id]++;
else//不等于说明这个人只有1块饼干,只有这一段减了1
for(int i=last.id+1;i<=now.id;i++)
ans[a[i].id]++;
now=last;
}
for(int i=1;i<=n;i++)
printf("%d ",ans[i]);
return 0;
}