[关闭]
@LittleRewriter 2017-10-08T02:21:56.000000Z 字数 3615 阅读 1634

DP选讲

懵逼 啥玩意 DP


QQ图片20171003182530.png

常见dp优化

单调队列优化

单调队列模板

  1. //L为头,R为尾,初始L=1,R=0
  2. void Insert(int x){
  3. while(l<=r&&a[r]<=x) r--;
  4. //由于寻找次数一定小于插入长度,所以保证复杂度O(n)
  5. s[++r]=x;
  6. }
  7. void Query(){
  8. return s[l];
  9. }
  10. void Delete(){
  11. //判断是否在单调队列中
  12. cnt++;
  13. if(t[l]==cnt) l++;
  14. }
  15. int main(){
  16. cin>>Q;
  17. while(Q--){
  18. cin>>A;
  19. if(A==1){
  20. cin>>x;
  21. Insert(x)
  22. }
  23. if(A==2){
  24. cout<<Query()<<endl;
  25. }
  26. if(A==3){
  27. Delete();
  28. }
  29. }
  30. }

多重背包

,将物品拆开
,将物品按照拆开
,单调队列
先枚举所有物品,再枚举所有可能情况,那么换一种写法:

  1. --仍然是伪代码
  2. v[i]来表示第i个物品的价值 w[i]来表示体积
  3. for (int i=1; i<=n; i++)
  4. for (int k=0; k<=m; k++)
  5. for (int j=0; j<=c[i]; j++)
  6. if (k>=j*w[i])
  7. dp[i][k]=max(dp[i][k],dp[i-1][k-j*w[i]]+j*v[i])

QQ截图20171003181115.png
来看红与蓝色,那么后一个转移来源与前一个的区别就是新添了一个少了一个,也就是线性维护这样几个操作:
插入队首元素
取出最大的元素
弹出队尾元素
就是单调队列

烽火传递

给定n个非负整数,选择其中若干数字,使得每连续k个数中至少有一个数被选出。要求选择的数字之和尽可能小。n<=1000。

dp[i]表示1~i是被选出的,那么dp[i]=min{dp[j]}+a[i]且
而每次转移只相差了a[i-k+1]与a[i+1]两个数,那么就是用一个线性结构维护这个操作,
这就是单调队列,也就是烽火传递可以O(n)实现

  1. --还是伪代码
  2. for(int i=l;i<=k;++i) dp[i]=a[i];
  3. for(int i=0;i<k;++i) push(dp[i]);//压入
  4. for(int i=k;i<=n;++i){
  5. dp[i]=query()+a[i];//找最小值
  6. push(dp[i]);
  7. pop();//删除队尾
  8. }

也就是,一个dp值有c[i]个等差数列推得,而转移以min/max移动
通式:dp[i][j]=min{dp[i-1][k]}

最大子段和

  1. for (int i=1; i<=n; i++)
  2. {
  3. sum+=a[i];
  4. if (sum<0) sum=0;
  5. MAX=max(MAX,sum);
  6. }
  7. cout<<MAX;
  1. for (int i=1; i<=n; i++)
  2. s[i]=s[i-1]+a[i];
  3. for (int i=1; i<=n; i++)
  4. {
  5. ans=max(ans,s[i]-MIN);
  6. MIN=min(MIN,s[i]);
  7. }

任何一段区间[L,R]相当于S[R]-S[L-1],我们就是找到最大的S[R]与最小的S[L-1],用min记录一下。

加强版 成环

装喷头

有一根长为n的数轴,要在这根数轴上装一些喷头,每个喷头的喷射半径在[a,b]之间并可调节。
其中有m个限制,形如li,ri,表示[li,ri]这一段区间的整点一定要同时被一个喷头喷到。
规定每个整点位置恰好被一个喷头喷到,并且喷头不允许喷出外。问至少装几个喷头。
n<=1000。

用dp[i]表示第i个位置装喷头,1~i都满足条件这样的最少喷头数量
这么搞转移列不出来
用dp[i]表示前i个位置都满足条件了,最少装多少喷头,dp[j]表示1~j都满足条件了,j+1~i没满足条件,装个喷头要恰好覆盖这些
由于喷头的喷水半径是个圆,所以2a<=i-j<=2b
因此dp[i] = min{dp[j]}+1 2a<=i-j<=2b
j的取值是有确定的,所以j是由区间构成的
标记时,若那么dp[i]=inf.

  1. for (int i=1; i<=n; i++)
  2. {
  3. if (!v[i]) dp[i]=inf;
  4. else
  5. {
  6. dp[i]=Query()+1;
  7. }
  8. Insert(dp[i+1-2a]);
  9. Delete(dp[i-2b]);
  10. }

NOI2005 瑰丽华尔兹

在一个n*m的矩阵上,有一些点是障碍,有一些点是空地。并且有一个点有个人。
这个人在[L1,R1]个时刻会向X1行动,[L2,R2]个时刻会向X2行动以此类推。总共有T段区间,且L[i+1]=R[i]+1,L[1]=1。
其中X1,X2,…,XT为上下左右中的一种。
为了使这个人在移动过程中不撞掉也不掉出矩阵,每次可以选择耗费1点能量让它不动,问最少耗费多少能量使得它再R[T]这个时刻还在这个矩阵上。
n,m<=200,T<=200,R[T]<=40000。

dp[i][j][k]第i时刻在(j,k)
枚举第i时刻用不用能量,可以转移为dp[i][j+v[i]][k+v[i]]
正确性可以保证,状态数比较大
枚举第i区间结束时在(j,k)用的能量
那么就是max{dp[i-1][j][k-x]+(t[i]-x)}(t[i]-x表示用了多少能量)
复杂度
用dp[i][j][k]表示第i个区间结束时最多走了多少步
以右方向为例来分析这个问题
那么t表示该时间段的时间且中途无障碍
即k>=k-x>=k-t也就是在[k-t,k]中转移
随k增加,让左右端点分别移动,这就是一个单调队列。
问题的关键是障碍,随着k的增加,会遇到障碍。如果走到障碍,本次不转移,左右端点分别移动到k+1,方便下一次运行。
具体实现时,由于由于要加x,就可以将队列中所有元素增加一,用一个变量维护一下。
然后是zhw本人的解释:

dp[i][j][k] 第i个时间段结束,人在(j,k)这个位置,这样子最多走多少步
假设 i这个时间段 这个人会往右走 // t=这一个时间段有多少时间
dp[i][j][k]=max{dp[i-1][j][k-x]+x} 0<=x<=t。(j,k-x)~(j,k) 没有障碍物
k-t<=p<=k 并且 (j,p)~(j,k)没有障碍物
dp[i][j][k]=max{dp[i-1][j][p]+x} k-t<=p<=k 并且 (j,p)~(j,k)没有障碍物
随着k的增加 k-t,k都一定会增加 单调队列
维护能更新的这个区间会怎么移动。 [l,r]
每次在插入进队列之前,把队列中所有元素+1,这件事是可以通过一个变量来维护的。
T*n^3
i这个时间段 这个人会往左、下、上走

一道神题

一个n*n的矩阵,每行每列恰好放两个棋子,问方案总数%p的值

似乎不能OEIS

似乎是本蒟蒻听懂的唯一一道题
dp[i][j][k]表示前i行有j列放了一个棋子,k列没有放棋子
对i+1行来说,
(0,0)表示放的两列之前都没放,则转化为dp[i+1][j+2][k-2]
(0,1)表示放的两列一个放过一个没放,则转化为dp[i+1][j][k-1]
(1,1)表示放的两列之前都放过一个,则转化为dp[i+1][j-2][k]

显然,



结果就是dp[n][0][0]
这样就是的做法。
再来思考一下,ijk之间有这样的关系

所以这里可以二维优化一维:



结果就是dp[n][0]
复杂度
如何达到O(n)的复杂度呢?
以块为单位,用dp[i]表示i*i的矩阵满足条件的方案总数
观察最后一列,前两个和任意两个是没有区别的
例如下图,这两种状态实质是相同的。
2017-10-3 16-39-34.JPG
,g(i)表示第一行第n个第二行第n个都放了的总数
而对于g[i],如果第一行和第二行其它棋子在同一列,那么g[i]+=dp[i-2]*(i-1),如下图
2017-10-3 16-42-52.JPG
否则,如下图,每一个状态移上去就可以满足了。即g[i]+=dp[i-1]*2.
2017-10-3 16-44-30.JPG
将上述结合一下,


我们完成了一个O(n)的方程。
综上所述,我们其实可以采用这样一种思路:

将原问题化简为与原问题相似规模较小的问题

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注