决策单调性(Flush Hu

这有什么用?

这能优化dp。

决策单调性和斜率优化差不多。

需要细心发现决策之间的递变规律。

比如:

决策单调性有两种做法:

1.二分栈(队列):

决策二分栈(一种单调栈)来维护所有有用的决策,其中栈顶是当前最优决策。

2.分治:

然而二分栈有一个局限性,那就是必须能快速计算。如果不能算的话,在求临界值k的时候复杂度会严重退化。

既然转移过程是单调并且离线的,我们考虑分治。假设当前我们求解一段区间,而所有的最优决策点在之间。对于的中点,我们可以暴力扫一遍,找到它的最优决策点。因为决策单调,所以的决策落在上,而的决策落在上,变成了两个规模减半的小问题。

3.四边形不等式优化 (一般不用):

利用的决策点<=的<=的。 在求解的时候就可以缩小决策点范围,做到O(n^2 * d),其中转移一次的复杂度为O(d)

例题:[POI2011]Lightning Conductor

题意:

已知一个长度为n的序列a1,a2,…,an。 对于每个1<=i<=n,找到最小的非负整数p满足 对于任意的j, aj < = ai + p – sqrt(abs(i-j))

题解:

单独看前一部分:

很明显是个要用决策单调性优化的式子。

对于每个,把看成关于的函数。我们要做的就是在所有的函数中找到最值。

img

观察发现,真正有用的函数只有最上面那个!然而实际情况比这个稍复杂些。sqrt的增速是递减的,因此可能存在一个j比较小的函数,在某一时刻被j比较大的函数反超。我们大概需要维护这样的若干个函数:

img

我们用队列实现决策二分栈 。按j从小到大依次维护这些函数。

显然,对于其中任意两个相邻的函数,它们都有一个临界值。显然序列中的也要严格递增。否则,如果,可以想象根本没有用。

先for一遍i,我们尝试着把加入队列。这时候为了保证k递增,设队尾决策为t,我们判断,如果(此时会有),那么t没用,出队。自己画下图就懂了。

该出去的都出去后,i就可以加入队尾了。这时候可以来求了。我们检查一下队首决策h,如果,说明h的巅峰时刻已经过去,出队。最后队首就是所有函数中的最大值。

代码:

#include
using namespace std;
#define suan(i,j) a[j]+sq[i-j]
const int N=5e5+5;
int n,a[N],q[N],k[N];
double p[N],sq[N];
/*char buf[1<<21],*p1=buf,*p2=buf;
inline int gc()
{
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;
}*/
#define gc getchar
inline int read()
{
    int ret=0,f=0;
    char c=gc();
    while(!isdigit(c))
    {
        if(c=='-')f=1;
        c=gc();
    }
    while(isdigit(c))
    {
        ret=ret*10+c-48;
        c=gc();
    }
    if(f)return -ret;
    return ret;
}
int erfen(int x,int y)
{
    int l=y,r=k[x]?k[x]:n,mid,ret=r+1;
    while(l<=r)
    {
        mid=(l+r)/2;
        if(suan(mid,x)<=suan(mid,y)){ret=mid,r=mid-1;}
        else l=mid+1;
    }
    return ret;
}
void work()
{
    int l=1,r=0;
    for(int i=1;i<=n;i++)
    {
        while(l<r&&suan(k[r-1],q[r])<suan(k[r-1],i))r--;
        k[r]=erfen(q[r],i);
        q[++r]=i;
        while(l<r&&k[l]<=i)l++;
        p[i]=max(p[i],suan(i,q[l]));
    }
}
int main()
{
    n=read();
    for(int i=1;i<=n;i++)
    {
        a[i]=read();
        sq[i]=sqrt(i);
    }
    work();
    for(int i=1;i<=n/2;i++)
    {
        swap(a[i],a[n-i+1]);
        swap(p[i],p[n-i+1]);
    }
    memset(k,0,sizeof(k));
    work();
    for(int i=n;i;--i)
        printf("%d\n",max((int)ceil(p[i])-a[i],0));
    return 0;
}

当然,分治也能做啊。

solve(l,r,L,R)表示f[l]f[r]的最优决策点在LR

令mid=(l+r)/2

O(n)扫一遍取到k[mid]

继续分治

solve(l,mid-1,L,k[mid])

solve (mid+1,r,k[mid],R)

代码:

#include
using namespace std;
const int N=5e5+5;
int a[N];
double f1[N],f2[N];
/*char buf[1<<21],*p1=buf,*p2=buf;
inline int gc()
{
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;
}*/
#define gc getchar
inline int read()
{
    int ret=0,f=0;
    char c=gc();
    while(!isdigit(c))
    {
        if(c=='-')f=1;
        c=gc();
    }
    while(isdigit(c))
    {
        ret=ret*10+c-48;
        c=gc();
    }
    if(f)return -ret;
    return ret;
}
void solve1(int l,int r,int L,int R)
{
    if(l>r)return;
    int mid=(l+r)/2,g=0;
    double tmp=0.0;
    f1[mid]=a[mid];
    for(int i=L;i<=min(R,mid);i++)
    {
        tmp=a[i]+sqrt(double(mid-i));
        if(tmp>f1[mid])
        {
            f1[mid]=tmp;
            g=i;
        }
    }
    if(g==0)g=mid;
    f1[mid]-=a[mid];
    solve1(l,mid-1,L,g); 
    solve1(mid+1,r,g,R);
}
void solve2(int l,int r,int L,int R)
{
    if(l>r)return;
    int mid=(l+r)/2,g=0;
    double tmp=0.0;
    f2[mid]=a[mid];
    for(int i=R;i>=max(mid,L);i--)
    {
        tmp=a[i]+sqrt(double(i-mid));
        if(tmp>f2[mid])
        {
            f2[mid]=tmp;
            g=i;
        }
    }
    if(g==0)g=mid;
    f2[mid]-=a[mid];
    solve2(l,mid-1,L,g);
    solve2(mid+1,r,g,R);
}
int main()
{
    int n=read();
    for(int i=1;i<=n;i++)a[i]=read();
    solve1(1,n,1,n);
    solve2(1,n,1,n);
    for(int i=1;i<=n;i++)
    printf("%lld \n",(int)ceil(max(f1[i],f2[i])));
    return 0;
}

再看道题:Ciel and Gondolas

题意:

将n个人分成K段,每段的人两两之间产生代价,求最小代价和

题解:

这题有更优的做法,这里就不讲了,只讲决策单调性优化dp。

其实就是前缀和

代码:

#include
using namespace std;
const int N=4040;
const int inf=0x3f3f3f3f;
int n,k,sum,s[N][N],K,dp[N][1000],q[N];
/*char buf[1<<21],*p1=buf,*p2=buf;
inline int gc()
{
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;
}*/
#define gc getchar
inline int read()
{
    int ret=0,f=0;
    char c=gc();
    while(!isdigit(c))
    {
        if(c=='-')f=1;
        c=gc();
    }
    while(isdigit(c))
    {
        ret=ret*10+c-48;
        c=gc();
    }
    if(f)return -ret;
    return ret;
}
inline int gao(int x,int y)
{
    return s[y][y]-s[y][x];
}
inline int suan(int x,int y)
{
    int l=y+1,r=n,res=n+1;
    while(l<=r)
    {
        int mid=(l+r)/2;
        if(dp[x][k-1]+gao(x,mid)>=dp[y][k-1]+gao(y,mid))
        {
            res=mid;
            r=mid-1;
        }
        else l=mid+1;
    }
    return res;
}
int main()
{
    n=read();K=read();
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(j<=i)
            {
                s[i][j]=read();
                sum+=s[i][j];
            }
            else read();
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)s[i][j]+=s[i-1][j];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)s[i][j]+=s[i][j-1];
    for(int i=0;i<=n;i++)dp[i][0]=inf;
    dp[0][0]=0;
    for(k=1;k<=K;k++)
    {
        int L=1,R=1;q[L]=0;
        for(int i=1,j;i<=n;i++)
        {
            while(L<R&&suan(q[L],q[L+1])<=i)L++;
            j=q[L];
            dp[i][k]=dp[j][k-1]+gao(j,i);
            while(L=suan(q[R],i))R--;
            q[++R]=i;
        }
    }
    printf("%d\n",dp[n][K]);
    return 0;
}

题目:[NOI2009]诗人小G

代码:

#include
#define suan(i,j) f[j]+qpow(abs(s[i]-s[j]-L))//计算函数值
using namespace std;
const int N=1e5+5;
int n,L,P,s[N],q[N],k[N],pr[N];
long double f[N];
char str[N][33];
/*char buf[1<<21],*p1=buf,*p2=buf;
inline int gc()
{
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;
}*/
#define gc getchar
inline int read()
{
    int ret=0,f=0;
    char c=gc();
    while(!isdigit(c))
    {
        if(c=='-')f=1;
        c=gc();
    }
    while(isdigit(c))
    {
        ret=ret*10+c-48;
        c=gc();
    }
    if(f)return -ret;
    return ret;
}
long double qpow(long double b)
{
    long double a=1;
    int k=P;
    while(k)
    {
        if(k%2==1)a=a*b;
        b=b*b;
        k=k/2;
    }
    return a;
}
int erfen(int x,int y)
{
    int l=x,r=n+1,mid;
    while(l<r)
    {
        mid=(l+r)/2;
        if(suan(mid,x)>=suan(mid,y))r=mid;
        else l=mid+1;
    }
    return r;
}
int main()
{
    int T=read();
    while(T--)
    {
        n=read();L=read()+1;P=read();
        for(int i=1;i<=n;i++)
        {
            scanf("%s",str[i]);
            s[i]=s[i-1]+strlen(str[i])+1;
        }
        int l=1,r=1;
        q[1]=0;
        for(int i=1;i<=n;i++)
        {
            while(l<r&&k[l]<=i)l++;
            f[i]=suan(i,q[l]);
            pr[i]=q[l];
            while(l=erfen(q[r],i))r--;
            k[r]=erfen(q[r],i);
            q[++r]=i;
        }
        if(f[n]>1e18)puts("Too hard to arrange");
        else{
            printf("%.0Lf\n",f[n]);
            q[r=0]=n;
            for(int i=n;i;q[++r]=i=pr[i]);
            for(;r;--r)
            {
                int i;
                for(i=q[r]+1;i<q[r-1];i++)
                    printf("%s ",str[i]);
                puts(str[i]);
            }
        }
        puts("--------------------");
    }
    return 0;
}

其他题目暂时咕咕