决策单调性优化DP

四边形不等式

如果一个 dp 数组满足四边形不等式 f [ a , c ] + f [ b , d ] &lt; = f [ a , d ] + f [ b , c ] f[a,c]+f[b,d]&lt;=f[a,d]+f[b,c] f[a,c]+f[b,d]<=f[a,d]+f[b,c] ( a &lt; b ≤ c &lt; d a&lt;b\leq c&lt;d a<bc<d,交叉小于包含),那么它满足决策单调性 s [ i , j ] &lt; = s [ i + 1 , j ] &lt; = s [ i + 1 , j + 1 ] s[i,j]&lt;=s[i+1,j]&lt;=s[i+1,j+1] s[i,j]<=s[i+1,j]<=s[i+1,j+1]。(证明见百度百科)。

如果 dp 方程满足 f [ i ] [ j ] = m i n { f [ i ] [ k ] + f [ k + 1 ] [ j ] } + c o s t ( j , i ) f[i][j]=min\{f[i][k]+f[k+1][j]\}+cost(j,i) f[i][j]=min{f[i][k]+f[k+1][j]}+cost(j,i) w w w 满足四边形不等式,且满足 w [ a , d ] &gt; = w [ b , c ] w[a,d]&gt;=w[b,c] w[a,d]>=w[b,c] a ≤ b ≤ c ≤ d a\leq b\leq c\leq d abcd。那么 f f f 也满足决策单调性。

如果 f [ i ] = min ⁡ { f [ j ] + c o s t ( j , i ) } f[i]=\min\{f[j]+cost(j,i)\} f[i]=min{f[j]+cost(j,i)} c o s t cost cost 满足四边形不等式(或者说代价为凸),也是有决策单调性的。

剩下的就是yy。

分治

当 dp 数组是从上一层转移过来的时候,可以用分治求解决策单调性问题。分治的一个好处是不需要快速计算任意 c o s t cost cost 函数,只需要快速转移相邻的 c o s t cost cost 即可。

做法是 s o l v e ( l , r , x , y ) solve(l,r,x,y) solve(l,r,x,y) 表示我们要转移 l , r l,r l,r 中的 dp 值,决策点在 x , y x,y x,y 范围里。每次我们暴力计算出 m i d mid mid 的最优决策点 p p p,递归计算 s o l v e ( l , m i d − 1 , x , p ) solve(l,mid-1,x,p) solve(l,mid1,x,p) s o l v e ( m i d + 1 , r , p , y ) solve(mid+1,r,p,y) solve(mid+1,r,p,y)

关于复杂度:对于决策点的枚举,每一层只会遍历数组一遍。对于价值函数的计算,从这一层的 [ x , y ] [x,y] [x,y] 到下一层的 [ x , p ] [x,p] [x,p],变化是区间长度级别的。枚举决策点的时候价值函数的计算是 O ( 1 ) O(1) O(1) 的。因此复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

例题

CF868F
给定一个序列,要把它分成k个子序列。每个子序列的费用是其中相同元素的对数。求所有子序列的费用之和的最小值。
n ≤ 100000 , k ≤ 20 n\leq 100000,k\leq 20 n100000,k20

分治写法的模板题。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int tong[100010],l=1,r,now,n,k,a[100010];
ll f[21][100010];
ll sum=0;
inline int read()
{
	char c=getchar();int x=0,flag=1;
	while(!isdigit(c)){if(c=='-') flag=-1;c=getchar();}
	while(isdigit(c)) x=x*10+c-'0',c=getchar();
	return x*flag;
}
void add(int x) {sum+=tong[a[x]]++;}
void del(int x) {sum-=--tong[a[x]];}
void check(int L,int R)
{
	while(r<R) add(++r);
	while(l>L) add(--l);
	while(r>R) del(r--);
	while(l<L) del(l++);
}
void solve(int l,int r,int x,int y)
{
	if(l>r) return;
	int mid=l+r>>1;
	int p=min(mid,y),k;
	for(int i=p;i>=x;i--)
	{
		check(i+1,mid);
		if(f[now-1][i]+sum<f[now][mid]) f[now][mid]=f[now-1][i]+sum,k=i;
	}
	solve(l,mid-1,x,k);
	solve(mid+1,r,k,y);
}
int main()
{
	n=read(),k=read(),now=0;
	for(int i=1;i<=n;i++) a[i]=read();
	memset(f,0x3f,sizeof(f));
	f[0][0]=0;
	for(int i=1;i<=k;i++) now++,solve(1,n,0,n);
	cout<<f[k][n];
	return 0;
}

单调栈

可以解决同层 dp 转移的问题,缺点是需要快速计算 c o s t cost cost

每个决策点影响到的一定是一个区间,因此用单调栈维护当前每个点的最优决策点。单调栈里是若干个区间 [ l , r , p ] [l,r,p] [l,r,p] 表示 l , r l,r l,r这个区间的最优决策点是 p p p。每次新加入一个决策点,我们它能否完全替代栈顶,是的话就弹栈,不是的话在这个区间里 erf,然后分裂成两个区间。

例题

诗人小G
给定一些字符串,划分成若组,每组的代价是字符串总长-给定常数的 p p p 次方。求最小代价和。

单调栈写法的模板题。

#include<bits/stdc++.h>
#define ld long double
#define ll long long
using namespace std;
const int N=100010;
struct poss{
    int l,r;
}pos[N];
ld f[N];
int n,L,p,pre[N],q[N],sum[N];
char s[N][33];
int read()
{
    int x=0;char c=getchar(),flag='+';
    while(!isdigit(c)) flag=c,c=getchar();
    while(isdigit(c)) x=x*10+c-'0',c=getchar();
    return flag=='-'?-x:x;
}
ld qpow(int a,int b)
{
    ld ans=1,base=a;
    while(b)
    {
        if(b&1) ans=ans*base;
        base=base*base;
        b>>=1;
    }
    return ans;
}
ld calc(int i,int x)
{
    return f[i]+qpow(abs(sum[x]-sum[i]-1-L),p);
}
void print(int x)
{
    if(pre[x]) print(pre[x]);
    for(int i=pre[x]+1;i<=x;i++) 
    {
        printf("%s",s[i]+1);
        if(i!=x) putchar(' ');
    }
    putchar('\n');
}
int main()
{
    int T=read();
    while(T--)
    {
        n=read(),L=read(),p=read();
        for(int i=1;i<=n;i++)
        {
            scanf("%s",s[i]+1);
            sum[i]=sum[i-1]+strlen(s[i]+1)+1;
        }
        int h=1,t=1;
        q[h]=0,pos[0].l=1,pos[0].r=n;
        for(int i=1;i<=n;i++)
        {
            while(pos[q[h]].r<i) h++;
            f[i]=f[q[h]]+qpow(abs(sum[i]-sum[q[h]]-1-L),p);
            pos[q[h]].l++,pre[i]=q[h];
            while(h<t&&calc(i,pos[q[t]].l)<=calc(q[t],pos[q[t]].l)) t--;
            int l=pos[q[t]].l,r=pos[q[t]].r,ans=pos[q[t]].r+1;
            while(l<=r)
            {
                int mid=l+r>>1;
                if(calc(i,mid)<=calc(q[t],mid)) ans=mid,r=mid-1;
                else l=mid+1;
            }
            if(ans<=n) 
            {
                pos[q[t]].r=ans-1;
                q[++t]=i;
                pos[i].l=ans;
                pos[i].r=n;
            }
        }
        if(f[n]>1e18) puts("Too hard to arrange");
        else printf("%lld\n",(long long)f[n]),print(n);
        puts("--------------------");
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值