DP优化总结

    • 矩阵优化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. i g(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
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(tail1,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。最小化买下所有的土地的费用。

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

DP优化总结_第1张图片

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
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].first0]+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;j1]=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]

斜率优化

模型:

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
#include
#include
#include
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=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/

你可能感兴趣的:(DP及DP优化)