DP优化总结

来源:互联网 发布:java poi 自适应宽度 编辑:程序博客网 时间:2024/04/27 20:33

    • 矩阵优化DP
      • 例子
        • fib数列
        • fib数列拓展
        • kmp转移
        • 小型图的转移
    • 决策单调栈优化
      • 例子
        • 玩具装箱Toy
        • 土地购买
    • 单调队列优化DP
      • 例子
        • 单调队列维护决策
        • 单调队列维护可选决策
        • 基环外向树的直径
        • 多重背包的OnmOnm优化
    • 斜率优化
      • 决策直线的斜率与二元组的横坐标同时满足单调性
      • 例题
        • 土地购买
        • 玩具装箱Toy
        • 仓库建设
        • 特别行动队
      • 不满足斜率单调性
        • 货币兑换Cash

矩阵优化DP

满足三个特点:
1.转移要选取的决策较少。(一般在常数级别)
2.转移的步骤很多。(一般是1e10以上的级别)
3.每一步的转移方程一样。(和递推类似)

*一般满足转移方程:

an+m=i=0m1an+ibi

例子:

fib数列

一般转移方程为f(n)=f(n1)+f(n2),决策只有两个,转移步骤一般很大,且每次转移都一样,满足优化条件。
不放设矩阵

A=[f(n1),f(n)]

每次要得到另一个矩阵

B=[f(n),f(n1)+f(n)]

转移矩阵已经很明显:

AT=B,T=[0111]

显然,Bn=A1Tn1。而矩阵满足快速幂。可在O(m3logn)时间内解决。(m是每次转移决策数,此时m=2)。

fib数列拓展:

1.f(n)=f(n2)+f(n1)+1

转移

[f(n1),f(n),1][f(n),f(n1)+f(n)+1,1]

转移矩阵

T=010110001.

时间复杂度O(33logn)

2.f(n)=f(n2)+f(n1)+n+1,s(n)f(n)s(n)

转移

[f(n1),f(n),n+1,1,s(n)][f(n),f(n1)+f(n)+n+2,n+2,1,s(n)+f(n+1)]

转移矩阵

T=0100111111001110001100001

时间复杂度O(53logn)

kmp转移

“GT考试”题解

小型图的转移

“迷路”题解

决策单调栈优化

如果转移满足两个特点:
1.转移方程:f(x)=mini=1x1f(i)+w[i,x](w[i,x])为i,x转移的代价(已知条件)。
2.w函数满足w[i,j]+w[i1,j1]w[i,j1]+w[i,j1](i<i1,j<j1)

那么称这个转移满足决策单调性:g(x)=k 表示 mini=1x1f(i)+w[i,x]k 处取得,那么ij,s.t.g(i)g(j)

w函数的性质一般难以观察,只用打表检验g(x)的性质即可。

考虑优化:
1.ig(x1)开始枚举。
有时候可以达到O(n)的线性复杂度,但是如果g(i)=1则退化为O(n2)

2.g(k)单调递增,对于每一个f(i),二分更新后面的g(k)(决策使用单调栈)。
严格O(nlogn)

例子

玩具装箱Toy

题意:
n个玩具,要将它们分为若干组进行打包,每个玩具有一个长度len[x]。每一组必须是连续的一组玩具。如果将第x到第y个玩具打包到一组,那么它们的长度 l=ji1+k=ijlen[k],将这组玩具打包所需的代价等于(lL)2 。问将所有玩具打包的最小代价是多少。注意到每组玩具个数并没有限制。n<=50000

题解:
打表观察到决策单调性后直接二分解决。这里贴一份代码:

#include<bits/stdc++.h>using namespace std;typedef pair<int,int> pii;typedef long long ll;streambuf *ib,*ob;inline int read(){    char ch=ib->sbumpc();int i=0,f=1;    while(!isdigit(ch)){if(ch=='-')f=-1;ch=ib->sbumpc();}    while(isdigit(ch)){i=(i<<1)+(i<<3)+ch-'0';ch=ib->sbumpc();}    return i*f;}int buf[80];inline void W(ll x){    if(!x){ob->sputc('-');return;}    if(x<0){ob->sputc('-');x=-x;}    while(x){buf[++buf[0]]=x%10,x/=10;}    while(buf[0])ob->sputc(buf[buf[0]--]+'0');}const int Maxn=5e4+50;int n,head,tail;ll len[Maxn],L,f[Maxn];pii q[Maxn];inline ll calc(int i,int j){return (j-i+len[j]-len[i-1]-L)*(j-i+len[j]-len[i-1]-L);}inline int solve(int o,int now,int l,int r){    int ans=0;    while(l<=r)    {        int mid=(l+r)>>1;        if(f[o]+calc(o+1,mid)>=f[now]+calc(now+1,mid))ans=mid,r=mid-1;        else l=mid+1;    }    return ans;}int main(){    ios::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);ib=cin.rdbuf();ob=cout.rdbuf();    n=read(),L=read();    for(int i=1;i<=n;i++)len[i]=1ll*read()+len[i-1];    q[head=tail=1]=make_pair(1,0);    for(int i=1;i<=n;i++)    {        f[i]=f[q[head].second]+calc(q[head].second+1,i);        ((++q[head].first)>=q[head+1].first&&tail>head)?(head++):0;        int pos=n;        while(tail>=head&&(f[i]+calc(i+1,q[tail].first)<=f[q[tail].second]+calc(q[tail].second+1,q[tail].first)))pos=q[tail].first,tail--;        if(tail<head)q[++tail]=make_pair(i+1,i);        else        {            pos=solve(q[tail].second,i,q[tail].first,pos);            if(pos)q[++tail]=make_pair(pos,i);        }    }    W(f[n]);ob->sputc('\n');} 

土地购买

题意:
n块土地需要购买,每块土地都是长方形的,有特定的长与宽。你可以 一次性购买一组土地,价格是这组土地中长的最大值乘以宽的最大值。比方说一块5*3 的土地和一块9*2的土地在一起购买的价格就是 9*3。最小化买下所有的土地的费用。

题解:
显然对于长宽都含于另一个长方体的长方体可以忽略。
那么剩下的长方体排布形式一定为:

ij,s.t.xi<xj,yi>yj
又有转移方程:f(i)=mink=0i1f(k)+w[k,i],其中w[k,i]=yk·xi,那么四边形不等式就很好证了:

x4·y1+x3·y2x4·y2+x3·y1(下标表示大小关系,x随i递减,y随j递增)

因为:x4(y1y2)x3(y1y2)得证。
满足决策单调性。

单调队列优化DP

如果转移满足以下模型:
f(x)=mini=b[x]x1g(i)+w[x](g(i)iw[x]x,b[x]x)

那么可以用单调队列维护决策表达到O(n)的时间复杂度。

怎么维护?
发现对于一个决策g(i)只会影响到一定的f(x),且越靠后面的g(i)影响的f(x)也越靠后面(因为b[x]随x不降)。那么一个决策如果比前面的决策更优,前面的决策就可以直接丢掉。(后面能够被丢掉决策更新的状态一定能被更优的该决策更新)。每次取队首元素,均摊O(1)

例子

单调队列维护决策

“生产商品”题解

单调队列维护可选决策

*这类dp满足可选决策时单调的,但最优决策要在可选决策中选取最优值,一般用set或平衡树(Splay\Treap)维护.
如:
poj3017:Cut the Sequence

•问题描述

给定一个有n个非负整数的数列a,要求将其划分为若干个部分,使得每部分的和不超过给定的常数m,并且所有部分的最大值的和最小。其中n<=105。
例:n=8, m=17,8个数分别为2 2 2 | 8 1 8 |1 2,答案为12,分割方案如图所示。

•解法分析

刚开始拿到这道题目,首先要读好题:最大值的和最小。
首先设计出一个动态规划的方法

f[i]=maxj=b[x]i1{f[j]+Maxnumber[j+1,i]}

其中f[i]代表把前i个数分割开来的最小代价。b[i]=min(j|sum[j+1,i]m)可以O(n)实现。
直接求解复杂度最坏情况下(M超大)是O(n2)的,优化势在必行。

几个性质

通过仔细观察,可以发现以下几点性质:
① 在计算状态f(x)的时候,如果一个决策k作为该状态的决策,那么可以发现第k个元素和第x个元素是不分在一组的。
b[x]随着x单调不降的,用这一点,可以想到什么?可以想到前面单调队列的一个限制条件。
③ 来看一个最重要的性质:如果一个决策k能够成为状态f(x)的最优决策,当且仅当a[k]>a[j],j[k+1,x]。为什么呢?其实证明非常非常容易(用到性质1),交给读者自己考虑。

单调队列优化可选决策

到此为止,我们可以这样做:由于性质三,每计算一个状态f(x),它的有效决策集肯定是一个元素值单调递减的序列,我们可以像单调队列那样每次在队首删除元素,直到队首在数列中的位置小于等于 ,然后将a[x]插入队尾,保持队列的元素单调性。

这时候问题来了,队首元素一定是最佳决策点吗?我们只保证了他的元素值最大……如果扫一遍队列,只是常数上的优化,一个递减序足以将它否决。

我们观察整个操作,将队列不断插入、不断删除。对于除了队尾的元素之外,每个队列中的元素供当前要计算的状态的“值”是 f(q[x].position)+a[q[x+1].position],其中,q[x]代表第x个队列元素,position这代表他在原来数组中的位置,我们不妨把这个值记为t。那每一次在队首、队尾的删除就相当于删除t,每一次删除完毕之后又要插入一个新的t,然后需要求出队列中的t的最小值。

我们发现,完成上述一系列工作的最佳选择就是平衡树,这样每个元素都插入、删除、查找各一遍,复杂度为O(logn),最后的时间复杂度是O(nlogn)

基环外向树的直径

例如:bzoj1791:Island
(这道题是真的难写。。)

首先,每一个基环外向树相当于一个环上有一些点挂了一颗子树,先把这个点的深度算出来,再考虑环上做DP的方法:

断开任意一条环边,再把这个环复制一遍,做b[x]=xn+1的dp就好了。
给一份代码,可以参考参考

#include<bits/stdc++.h>using namespace std;namespace IO{    streambuf *ib,*ob;    int buf[50];    inline void init()    {        ios::sync_with_stdio(false);        cin.tie(NULL);cout.tie(NULL);        ib=cin.rdbuf();ob=cout.rdbuf();    }    inline int read()    {        char ch=ib->sbumpc();int i=0,f=1;        while(!isdigit(ch)){if(ch=='-')f=-1;ch=ib->sbumpc();}        while(isdigit(ch)){i=(i<<1)+(i<<3)+ch-'0';ch=ib->sbumpc();}        return i*f;    }    inline void W(long long x)    {        if(!x){ob->sputc('0');return;}        if(x<0){ob->sputc('-');x=-x;}        while(x){buf[++buf[0]]=x%10;x/=10;}        while(buf[0])ob->sputc(buf[buf[0]--]+'0');    }}typedef long long ll;typedef pair<int,ll> pil;typedef pair<int,int> pii;const int Maxn=1e6+50;int n,ecnt=1,tail,tailtmp,bz,bg,last[Maxn],from[Maxn],vis[Maxn],ins[Maxn],id[Maxn];ll dp[Maxn][2],ans,res;pii statmp[Maxn];pil sta[Maxn];struct E{int to,val,nxt;}edge[Maxn*2];inline void add(int x,int y,int z){    edge[++ecnt].to=y;edge[ecnt].nxt=last[x];last[x]=ecnt;edge[ecnt].val=z;    edge[++ecnt].to=x;edge[ecnt].nxt=last[y];last[y]=ecnt;edge[ecnt].val=z;}inline void dfs(int now,int val){    vis[now]=1;statmp[++tailtmp]=make_pair(now,val);id[now]=tailtmp;    for(int e=last[now];e;e=edge[e].nxt)    {        int v=edge[e].to;        if((e^1)==from[now])continue;        if(!vis[v])from[v]=e,dfs(v,edge[e].val);        else {statmp[id[v]].second=edge[e].val;bz=1;bg=id[v];return;}        if(bz)return;    }    tailtmp--;}inline void dfs2(int now,int f=0){    vis[now]=1;    for(int e=last[now];e;e=edge[e].nxt)    {        int v=edge[e].to;        if(ins[v]||v==f)continue;        dfs2(v,now);        ll t=dp[v][0]+edge[e].val;        if(t>=dp[now][0])dp[now][1]=dp[now][0],dp[now][0]=t;        else if(t>dp[now][1])dp[now][1]=t;        ans=max(ans,dp[now][1]+dp[now][0]);    }}pil q[Maxn*2];int qhead,qtail;inline ll calc(int i){    for(int i=bg;i<=tailtmp;i++)sta[++tail]=statmp[i],ins[sta[tail].first]=1;    for(int i=1;i<=tail;i++)dfs2(sta[i].first);    memcpy(sta+tail+1,sta+1,sizeof(pil)*tail);int len=tail*2;    for(int i=1;i<=len;i++)sta[i].second+=sta[i-1].second;    ans=max(ans,dp[sta[1].first][0]);q[qhead=qtail=1]=make_pair(1,dp[sta[1].first][0]-sta[1].second);    for(int i=2;i<=len;i++)    {        int lim=i-tail+1;        while(q[qhead].first<lim)        qhead++;        ll t=dp[sta[i].first][0]+q[qhead].second+sta[i].second;        ans=max(ans,t);        t=dp[sta[i].first][0]-sta[i].second;        while(qhead<=qtail&&q[qtail].second<=t)qtail--;        q[++qtail]=make_pair(i,t);    }    return ans;}int main(){    IO::init();n=IO::read();    for(int i=1;i<=n;i++)    {        int y=IO::read(),z=IO::read();        add(i,y,z);    }    for(int i=1;i<=n;i++)    {        if(!vis[i])        {            tailtmp=bg=bz=tail=ans=0;            dfs(i,0);            res+=calc(i);        }    }    IO::W(res);IO::ob->sputc('\n');}

多重背包的(Onm)优化

首先,多重背包的转移方程是:

f[i][x]=maxs[i]k=0{f[i1][xkc[i]]+kw[i]}

如果决策连续就是裸的单调队列。现在考虑不连续:
首先可以发现一个x只会取与它模c[i]相同的状态,那么在模意义下分别DP就好了。

    for(int i=1;i<=n;i++)    {        for(int j=0;j<c[i];j++)        {            q[head=tail=1]=make_pair(f[j],0);            for(int k=j+c[i];k<=m;k+=c[i])            {                int a=k/c[i],t=f[k]-a*w[i];                while(head<=tail&&q[tail].first<=t)tail--;                q[++tail]=make_pair(t,a);                while(head<=tail&&q[head].second+s[i]<a)head++;                f[k]=max(f[k],q[head].first+a*w[i]);            }        }    }

斜率优化

模型:

f[i]=minj=1i1{a[i]g(j)+b[i]h(j)}

这个模型写的比较抽象, 其实它的涵盖范围是很广的。 首先, a[i], b[i]不一定要是常量, 只要他们与决策无关, 都可以接受; 另外, g(j)和 h(j)不管是常量还是变量都没有关系, 只要他们是一个由i决定的二元组就可以了。

为了方便描述, 把这个模型做如下转化:

P=f[i],x=g(j),y=h(j)y=abx+Pb

可以发现,状态现在就是许多条直线,而我们的任务是选出一条直线,使它经过前面出现的某一点,且的纵截距最小。 可以想象有一组斜率相同的直线自负无穷向上平移, 所碰到的第一个数据点就是最优决策。

这个时候, 有一个重要的性质, 那就是: 所有最优决策点都在平面点集的凸包上

基于这个事实, 我们可以开发出很多令人满意的算法。
这时, 根据直线斜率与数据点分布的特征, 可以划分为两种情况:

1.决策直线的斜率与二元组的横坐标同时满足单调性。

这样的模型是比较好处理的, 因为这个时候由于斜率变化是单调的, 所以决策点必然在凸壳上单调移动。我们只需要维护一个单调队列和一个决策指针,每次决策时作这样几件事:

1.决策指针( 也就是队首) 后移, 直至最佳决策点。
2.进行决策。
3.将进行决策之后的新状态的二元组加入队尾, 同时作GrahamScan式的更新操作维护凸壳。
算法的时间复杂度为 O(n)

例题

土地购买

再来观察这个转移方程:

f[i]=minj=1i1{f[j]+x[i]y[j+1]}

转化后:
f[j]=x[i]y[j+1]+f[i]

很明显,斜率x[i]单调递减,y[j+1]单调递减,满足条件。上斜率优化就行了。

#include<iostream>#include<cmath>#include<algorithm>#include<cstdio>using namespace std;const int Maxn=5e4+50;streambuf *ib,*ob;typedef long long ll;inline void init(){    ios::sync_with_stdio(false);    cin.tie(NULL);cout.tie(NULL);    ib=cin.rdbuf();ob=cout.rdbuf();}inline int read(){    char ch=ib->sbumpc();int i=0,f=1;    while(!isdigit(ch)){if(ch=='-')f=-1;ch=ib->sbumpc();}    while(isdigit(ch)){i=(i<<1)+(i<<3)+ch-'0';ch=ib->sbumpc();}    return i*f;}int buf[50];inline void W(ll x){    if(!x){ob->sputc('0');return;}    if(x<0){ob->sputc('-');x=-x;}    while(x)buf[++buf[0]]=x%10,x/=10;    while(buf[0])ob->sputc(buf[buf[0]--]+'0');}struct point{    ll x,y;    point(ll x=0,ll y=0):x(x),y(y){}    friend inline point operator -(const point &a,const point &b)    {        return point(a.x-b.x,a.y-b.y);    }    friend inline ll dot(const point &a,const point &b)    {        return a.x*b.y-a.y*b.x;    }}p[Maxn],q[Maxn];int n,tot,head,tail;ll f[Maxn];inline bool comp(const point &a,const point &b){    return a.y>b.y||(a.y==b.y&&a.x>b.x);}inline ll calc(int pos,int x){    return q[pos].y+p[x].x*q[pos].x;}int main(){    init();    n=read();    for(int i=1;i<=n;i++)    {        int x=read(),y=read();        p[i]=point(x,y);    }    sort(p+1,p+n+1,comp);    int mx=p[tot=1].x;    for(int i=2;i<=n;i++)    {        if(p[i].x<=mx)continue;        p[++tot]=p[i];mx=p[i].x;    }    q[head=tail=1]=point(p[1].y,0);    for(int i=1;i<=tot;i++)    {        while(head<tail&&calc(head,i)>=calc(head+1,i))head++;        f[i]=calc(head,i);        while(tail>=2&&dot(point(p[i+1].y,f[i])-q[tail-1],q[tail]-q[tail-1])<=0)tail--;        q[++tail]=point(p[i+1].y,f[i]);        if(head>tail)head=tail;    }    W(f[tot]);ob->sputc('\n');}

玩具装箱Toy

转移方程:

f[i]=minj=1i1{f[j]+(ij1+sum[i]sum[j]L)2}[i]=i+sum[i]L1,b[j]=sum[j]+jf[i]=f[j]+(a[i]b[j])2f[i]a[i]2=(f[j]+b[j]2)2(a[i]b[j])

x=b[j],y=f[j]+b[j]2,显然2a[i]单调增,x单调增,可以上板了。

仓库建设

设所有前面的仓库转移到该仓库花费费用为Dilivercost[i],前面仓库的容量总和为Sum[i],该仓库距离为i,建造所花费用Buildcost[i]。那么有转移:

f[i]=minj=0i1{f[j]+Dilivercost[i]Dilivercost[j](Sum[j](dis[i]dis[j]))+Buildcost[i]}

x=sum[j],y=f[j]Dilivercost[j]+sum[j]dis[j]不难发现x单调递增。
又有:
f[i]=min{ydis[i]x}+Buildcost[i]+Dilivercost[i]

dis[i]也满足单增,可以套板了。

特别行动队

同样写出来可以斜率优化。
(a只有小于0维护上凸包,若a大于0则维护下凸包)

不满足斜率单调性

这个没什么好说,用平衡树维护,做到nlogn(CDQ分治也可以做)

货币兑换Cash

转移方程

f[i]=max{f[i1],maxj=1i1{A[i]x[i]+B[i]y[i]}}

不满足单调,一般两种解法:
1.CDQ分治,保证处理完左边之后维护左半凸包并处理右边:http://paste.ubuntu.com/25699099/

2.set(平衡树)维护凸包,当然这种操作没有CDQ简便,最好写CDQ.:http://paste.ubuntu.com/25699108/