决策单调性优化dp学习笔记
决策单调性优化dp学习笔记
@
决策单调性的定义
四边形不等式
定义1.1.1:
若函数对于,其中,都有,则称函数满足四边形不等式
如果我们考虑用图形来表达,那么可以简记为“交叉”和“包含”的关系
这是四边形不等式最基本的定义。但是在做题中我们常常遇到下面的一种形式.
推论1.1.1:
若函数对于,其中,都有,则称函数满足四边形不等式
证明:
对于,有:
对于,有:
两式相加,得:
整理得:
以此类推,可得
同理对第二个数做这样的证明,即可得到
另外,如果函数满足四边形不等式,我们也称它满足凸完全单调性,或者说它是凸函数。关于函数凹凸性的讨论超出了本文的讨论范围,这里不再讨论
四边形不等式与决策单调性
定义1.2.1:对于形如的状态转移方程,记为取到最小值时的值.即为的最优决策。如果在上单调不减,则称具有决策单调性
定理1.2:对于形如的状态转移方程,若函数满足四边形不等式,则具有决策单调性
,根据决策单调性的定义得:
(即前的决策都没有优)
,显然,由于满足四边形不等式,那么有
移项,得
得
那么对于来说,以作为的决策,比作为的决策更优。因此的最优决策不可能小于,即.有决策单调性
于是,我们得到了证明一个dp方程满足决策单调性的方法:
- 证明权函数满足四边形不等式(比赛中可以采用打表估(xia)计(cai)的方法)
- 根据定理1.2,状态转移方程满足决策单调性
然而这样有什么用呢?根据决策单调性,我们可以把原来的的dp优化到甚至是
决策单调性的通用解法:单调队列+二分查找
[BZOJ 1563] [NOI 2009] 诗人小G
一首诗包含了若干个句子,对于一些连续的短句,可以将它们用空格隔开并放在一行中,注意一行中可以放的句子数目是没有限制的。小 G 给每首诗定义了一个行标准长度(行的长度为一行中符号的总个数),他希望排版后每行的长度都和行标准长度相差不远。显然排版时,不应改变原有的句子顺序,并且小 G 不允许把一个句子分在两行或者更多的行内。在满足上面两个条件的情况下,小 G 对于排版中的每行定义了一个不协调度, 为这行的实际长度与行标准长度差值绝对值的 P 次方,而一个排版的不协调度为所有行不协调度的总和。
小 G 最近又作了几首诗,现在请你对这首诗进行排版,使得排版后的诗尽量协调(即不协调度尽量小),并把排版的结果告诉他
转移方程推导
显然,设为选了第个句子并在此换行的最小不协调度。每句诗的长度为,为前句诗的总长度,那么
后面的式子表示把第句分成一行的代价。句子长度为,空格长度为
这里的函数为,由于的次数较高,无法斜率优化。于是尝试证明满足四边形不等式
决策单调性证明
我们要证明
移项,得
记:
则只需证明
即证明对于任意常数,函数单调递减.证明比较繁琐,这里引用一下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1L1uLFbE-1578552752630)(https://i.loli.net/2019/12/16/wLXR9QsjvOgUkF8.jpg)]
总之,满足四边形不等式,那么有决策单调性
优化方法
由于单调性,每个决策肯定存在一个区间使得当前情况下,
记表示当前情况下,第一个以为决策点不如以为决策点更优的位置(如果当前只计算到,则对于,)。则.可以二分查找求出。
我们维护一个单调队列存储决策点。在处理时,我们这样做:
-
如果队头的决策点对应区间不包含i,即则出队
-
通过队头决策点转移
-
通过二分寻找出最左边的,以为决策点不如以i为决策点更优的位置。这个位置实际上是.由于决策单调性,目前从这个位置往右的 dp 都满足以i为决策点是最优的。再二分出,如果,说明决策点对应的所有转移都不如更优,我们把出队,继续比较下一个决策点
-
当队尾的弹出停止的时候,将入队,且对应区间右端点为
#include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #define maxn 500000 #define maxl 30 #define INF 1e18 using namespace std; typedef long double db; int T; int n,L,P; char s[maxn+5][maxl+5]; int sum[maxn+5]; db dp[maxn+5]; int res[maxn+5]; inline db fast_pow(db x,int k){ db ans=1; while(k){ if(k&1) ans=ans*x; x=x*x; k>>=1; } return ans; } inline db calc(int j,int i){//计算f[j]+val(j,i) return dp[j]+fast_pow(abs(sum[i]-sum[j]+(i-j-1)-L),P); } inline int bin_search(int a,int b){//找到第一个决策b比决策a优的位置 if(calc(a,n)<calc(b,n)) return n+1; int l=b,r=n; int ans=-1; int mid; while(l<=r){ mid=(l+r)>>1; if(calc(b,mid)<=calc(a,mid)){ ans=mid; r=mid-1; }else l=mid+1; } return ans; } void ini(){ for(int i=1;i<=n;i++){ dp[i]=INF; res[i]=0; } } int q[maxn+5]; int stk[maxn+5];//找出1~n最优决策的每一段 int main(){ scanf("%d",&T); while(T--){ scanf("%d %d %d",&n,&L,&P); ini(); for(int i=1;i<=n;i++){ scanf("%s",s[i]); sum[i]=strlen(s[i])+sum[i-1]; } int head=1,tail=0; q[++tail]=0; dp[0]=0; for(int i=1;i<=n;i++){ while(head<tail&&bin_search(q[head],q[head+1])<=i) head++; //使得head决策点的对应区间包含i res[i]=q[head]; dp[i]=calc(q[head],i); while(head<tail&&bin_search(q[tail-1],q[tail])>=bin_search(q[tail],i)) tail--; //把以队尾决策点为决策点不如以i为决策点更优的位置出队 q[++tail]=i; //并替换成i } if(dp[n]>INF){ printf("Too hard to arrange\n"); }else{ printf("%lld\n",(long long)dp[n]); // int top=0; // for(int i=n;i;i=res[i]) stk[++top]=i; // stk[++top]=0; // for(int i=top-1;i>=1;i--){ // int r=stk[i],l=stk[i+1]+1; // for(int j=l;j<r;j++) printf("%s ",s[j]); // printf("%s\n",s[r]); // } } printf("--------------------\n"); } }
[UOJ 285]数据分块鸡(法1)
给出一个长度为的序列,编号为.还有个询问,表示求中的元素和。现在要利用分块的思想,可以选择任意位置作为分割点把序列分为多块。询问时一个块的代价是1.对于不足一个块的部分,若这个块中需要求和的区间小于这个块大小的一半,则代价为该区间长度。否则可以用块的总和相减,代价为块大小-该区间长度。
求如何分块使得所有询问的代价之和最小
转移方程推导
我们考虑每个块对答案的贡献,设表示把分为一块对答案的贡献。设表示已经分好,最后一个分割点在的最小代价,那么显然有:
这个方程的形式和上一题几乎一样。容(da)易(dan)证(cai)明(xiang)满足四边形不等式,因此该方程满足决策单调性,可以直接套用上一题的方法求解。
那么如何求?可以大力分类讨论询问区间与分块区间的关系(相交、包含),一共有六种情况(实际上原题题面已经指明了分类讨论的方向,具体方法见下面代码)
//query(p,q,r,s)表示查询左端点在[p,q],右端点在[r,s]中区间(.cnt表示这样的区间个数 .suml表示区间的左端点之和 .sumr表示区间的右端点之和 ll val(int l,int r) { //查询块[l,r)对答案的贡献 ll ans=0; int mid=(l+r)>>1; val_type t; //询问区间[x,y)包含[l,r),每个询问贡献1 t=query(1,l,r,n); ans+=t.cnt; //询问区间[x,y)被[l,r)包含,每个询问贡献(x-l)+(r-y) t=query(l,r,l,r); ans+=t.cnt*(r-l)+t.sumx-t.sumy; //询问区间[x,y)的右边和[l,r)相交,且相交部分不到[l,r)的一半,每个询问贡献(y-(l-1)) t=query(1,l-1,l,mid-(r-l+1)%2); ans+=t.sumy-t.cnt*(l-1); //询问区间[x,y)的右边和[l,r)相交,且相交部分超过[l,r)的一半,每个询问贡献(r-y) t=query(1,l-1,mid+1-(r-l+1)%2,r); ans+=t.cnt*r-t.sumy; //询问区间[x,y)的左边与[l,r)相交,且相交部分不到[l,r)的一半, 每个询问贡献((r+1)-x) t=query(mid+1,r,r+1,n); ans+=(r+1)*t.cnt-t.sumx; //询问区间[x,y)的左边与[l,r)相交,且相交部分超过[l,r)的一半, 每个询问贡献(x-l t=query(l+1,mid,r+1,n); ans+=t.sumx-l*t.cnt; return ans; }
其中的query
函数相当于一个二维前缀和。由于数据范围较大,可以用可持久化线段树或二维线段树实现。详见代码。
另外由于可持久化线段树的查询,该方法的的复杂度是。因此要格外注意常数优化。一个很强的优化是,对于队列中的每个节点,我们把它作为最优决策的区间存储下来,这样就不需要出入队的时候再二分去计算。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<vector> #define maxn 100000 #define maxlogn 30 using namespace std; typedef long long ll; inline void qread(int &x){ x=0; int sign=1; char c=getchar(); while(c<'0'||c>'9'){ if(c=='-') sign=-1; c=getchar(); } while(c>='0'&&c<='9'){ x=x*10+c-'0'; c=getchar(); } x=x*sign; } int n,m; struct val_type { ll cnt;//区间个数 ll sumx;//区间左端点之和 ll sumy;//区间右端点之和 val_type() { } val_type(int _cnt,ll _suml,ll _sumr) { cnt=_cnt; sumx=_suml; sumy=_sumr; } friend val_type operator + (val_type p,val_type q) { return val_type(p.cnt+q.cnt,p.sumx+q.sumx,p.sumy+q.sumy); } friend val_type operator - (val_type p,val_type q) { return val_type(p.cnt-q.cnt,p.sumx-q.sumx,p.sumy-q.sumy); } }; struct persist_segment_tree { #define lson(x) (tree[x].ls) #define rson(x) (tree[x].rs) struct node { int ls; int rs; val_type val; } tree[maxn*maxlogn+5]; int ptr=0; inline void push_up(int x) { tree[x].val=tree[lson(x)].val+tree[rson(x)].val; } void update(int &x,int last,int upos,int uval,int l,int r) { //插入二元组[upos,uval) x=++ptr; tree[x]=tree[last]; if(l==r) { tree[x].val.cnt++; tree[x].val.sumx+=upos; tree[x].val.sumy+=uval; return; } int mid=(l+r)>>1; if(uval<=mid) update(lson(x),lson(last),upos,uval,l,mid); else update(rson(x),rson(last),upos,uval,mid+1,r); push_up(x); } val_type get_sum(int xl,int xr,int L,int R,int l,int r) { if(xr==0) return val_type(0,0,0); if(L<=l&&R>=r) return tree[xr].val-tree[xl].val; int mid=(l+r)>>1; val_type ans=val_type(0,0,0); if(L<=mid) ans=ans+get_sum(lson(xl),lson(xr),L,R,l,mid); if(R>mid) ans=ans+get_sum(rson(xl),rson(xr),L,R,mid+1,r); return ans; } } T; int root[maxn+5]; inline val_type query(int lx,int rx,int ly,int ry) { if(lx>rx||ly>ry) return val_type(0,0,0); return T.get_sum(root[lx-1],root[rx],ly,ry,1,n); } ll val(int l,int r) { //查询块[l,r)对答案的贡献 ll ans=0; int mid=(l+r)>>1; val_type t; //询问区间[x,y)包含[l,r),每个询问贡献1 t=query(1,l,r,n); ans+=t.cnt; //询问区间[x,y)被[l,r)包含,每个询问贡献(x-l)+(r-y) t=query(l,r,l,r); ans+=t.cnt*(r-l)+t.sumx-t.sumy; //询问区间[x,y)的右边和[l,r)相交,且相交部分不到[l,r)的一半,每个询问贡献(y-(l-1)) t=query(1,l-1,l,mid-(r-l+1)%2); ans+=t.sumy-t.cnt*(l-1); //询问区间[x,y)的右边和[l,r)相交,且相交部分超过[l,r)的一半,每个询问贡献(r-y) t=query(1,l-1,mid+1-(r-l+1)%2,r); ans+=t.cnt*r-t.sumy; //询问区间[x,y)的左边与[l,r)相交,且相交部分不到[l,r)的一半, 每个询问贡献((r+1)-x) t=query(mid+1,r,r+1,n); ans+=(r+1)*t.cnt-t.sumx; //询问区间[x,y)的左边与[l,r)相交,且相交部分超过[l,r)的一半, 每个询问贡献(x-l t=query(l+1,mid,r+1,n); ans+=t.sumx-l*t.cnt; return ans; } ll dp[maxn+5]; ll calc(int x,int y) { return dp[x]+val(x+1,y); } vector<int>seg[maxn+5]; int q[maxn+5]; int pos[maxn+5]; int head=1,tail=0; inline int bin_search(int i,int a,int b) { int l=a,r=b; int ans=n+1; while(l<=r){ int mid=(l+r)>>1; if(calc(q[tail],mid)>=calc(i,mid)){ r=mid-1; ans=mid; }else l=mid+1; } return ans; } int main() { // freopen("7.in","r",stdin); int l,r; qread(n); qread(m); n--;//开区间 for(int i=1; i<=m; i++) { qread(l); qread(r); seg[l].push_back(r-1);//开区间 } for(int i=1; i<=n; i++){ root[i]=root[i-1]; for(int j=0;j<(int)seg[i].size();j++){ T.update(root[i],root[i],i,seg[i][j],1,n); } } memset(dp,0x3f,sizeof(dp)); dp[0]=0; q[++tail]=0; for(int i=1;i<=n;i++){ dp[i]=calc(q[head],i); if(i==n) break; pos[head]=max(pos[head],i+1); while(head<tail&&pos[head]>=pos[head+1]) head++; int rr=n+1; while(head<=tail&&calc(q[tail],pos[tail])>=calc(i,pos[tail])){ //如果i为最优决策的区间包含q[tail]为最优决策的区间,则出队 rr=pos[tail]; tail--; } if(head>tail){ q[++tail]=i; pos[tail]=i+1; continue; } int ll=pos[tail]+1;//现在i为最优决策的区间不包含q[tail]的区间,因此我们对于q[tail]和i,找到最优决策的分界点 int p=bin_search(i,ll,rr);//找到第一个i比q[tail]优的位置 if(p<=n){ q[++tail]=i; pos[tail]=p; } } printf("%lld\n",dp[n]); }
决策单调性的特殊解法:斜率优化
先简单复习一下斜率优化:
斜率优化是决策单调性问题一种衍生算法,一类特殊的决策单调性问题可以利用斜率
优化在线性时间内得到解决。
将一个决策 看作平面上的一个点。对于i来说,,当且仅当
其中均在计算后已知,已知
实现这一算法的一般方法为维护所有决策点构成的凸包
- 当单调递增时可以利用单调队列直接维护凸包,询问时只需弹出队首不满足条件的元素即可。 时间复杂度
- 当单调递增时,在凸包上二分寻找第一个斜率不超过的位置,时间复杂度
- 当均不单调递增时可以使用cdq分治.时间复杂度
[ARC 66D]Contest with Drinks Hard(弱化版)
在一场比赛中一共有道题,其中解决第道题需要花费秒。你可以在所有题中挑选
任意一些题并解决它们。定义一种解决方案所得的分数为满足 第道题被解决
且的对数减去解决问题所需要的时间和。
求最高分数
斜率优化推导
设为序列的前缀和,表示对于前i个位置其中第i个不选的最大价值,假设枚举j表示上一个不选的位置是j。那么成为了一个满足题面条件的区间,增加的对数为,那么
化成
决策点为,斜率为,最小化截距,可以直接斜率优化
决策单调性的特殊解法:整体分治
[ARC 66D]Contest with Drinks Hard
在一场比赛中一共有道题,其中解决第道题需要花费秒。你可以在所有题中挑选
任意一些题并解决它们。定义一种解决方案所得的分数为满足 第道题被解决
且的对数减去解决问题所需要的时间和。
现给出组询问,第i组询问需要你求出将第道题的解决时间变为秒后,分数最高
方案的得分(询问后序列还原)。
朴素情况的斜率优化见上
带修改的情况
考虑将pos位置改为x,那么原来要么选了,要么没有选
- 不选,那么只需要预处理出前缀和后缀的dp值和,方程类似朴素情况,那么答案就是
- 选了,那么要满足选的数构成的区间包含pos。考虑预处理出表示强制选第个的dp值,那么答案就是
考虑如何求.直接枚举跨越的区间比较难办,可以分治,每个分治区间[l,r]只讨论:前缀意义下,选了上一个不选的在[l,mid],最后一个选的在[mid+1,r]的答案。以及后缀意义下上一个不选的在[mid+1,r],最后一个选的在[l,mid]的答案
具体的说,我们讨论前缀意义下的情况:类似cdq分治的斜率优化,先把的点按建出凸包,对于的点,在凸包上查询截距最小值,求出对应的之后再加上,就是的一个可能值,用这个值去更新答案即可。后缀意义同理。
代码实现中需要注意边界(毒瘤!)。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxn 300000 #define INF 0x3f3f3f3f3f3f3f3f using namespace std; typedef long long ll; int n,m; ll a[maxn+5],sum[maxn+5]; ll f[maxn+5],g[maxn+5],h[maxn+5]; int top,s[maxn+5]; //注意为了避免分数,dp值被乘了2 inline ll fk(ll i) { return i; } inline ll fx(ll j) { return 2ll*j; } inline ll fc(ll i) { return i*i+i-2*sum[i]; } inline ll fy(ll j) { return 2*sum[j]+f[j]-j+j*j; } double fslope(ll x1,ll x2) { return 1.0*(fy(x2)-fy(x1))/(fx(x2)-fx(x1)); } inline ll gk(ll i) { return -i; } inline ll gx(ll j) { return -2*j; } inline ll gc(ll i) { return i*i-i+2*sum[i-1]; } inline ll gy(ll j) { return -2*sum[j-1]+g[j]+j+j*j; } double gslope(ll x1,ll x2) { return 1.0*(gy(x2)-gy(x1))/(gx(x2)-gx(x1)); } void pre_dp() { //斜率优化模板 f[0]=0; top=0; s[++top]=0; for(int i=1; i<=n; i++) { while(top>1&&fslope(s[top-1],s[top])<=fk(i)) top--; f[i]=max(f[i-1],fc(i)-fx(s[top])*fk(i)+fy(s[top])); while(top>1&&fslope(s[top],i)>=fslope(s[top-1],s[top])) top--; s[++top]=i; } //倒过来跑一次 g[n+1]=0; top=0; s[++top]=n+1; for(int i=n; i>=1; i--) { while(top>1&&gslope(s[top-1],s[top])<=gk(i)) top--; g[i]=max(g[i+1],gc(i)-gx(s[top])*gk(i)+gy(s[top])); while(top>1&&gslope(s[top],i)>=gslope(s[top-1],s[top])) top--; s[++top]=i; } } ll tmp[maxn+5]; void solve(int l,int r) { if(l==r) { h[l]=f[l+1]-2*a[l]+g[l+1]+2; //本来应该是-t[l]+1 return; } int mid=(l+r)>>1; solve(l,mid); solve(mid+1,r); //更新跨过mid的答案 //把[l,mid-1]建成凸包,再用[mid+1,r]中的点的f计算答案 top=0; s[++top]=l-1; for(int i=l; i<mid; i++) { while(top>1&&fslope(s[top-1],s[top])<=fk(i)) top--; while(top>1&&fslope(s[top],i)>=fslope(s[top-1],s[top])) top--; s[++top]=i; } for(int i=mid+1; i<=r; i++) { while(top>1&&fslope(s[top-1],s[top])<=fk(i)) top--; tmp[i]=fc(i)-fk(i)*fx(s[top])+fy(s[top])+g[i+1]; } ll maxt=-INF;//强行选可能造成负数 for(int i=r; i>mid; i--) { maxt=max(maxt,tmp[i]); h[i]=max(h[i],maxt);//注意选i,也有可能结尾不在i而在i后面,所以要记录一个后缀最大值 } //把[mid+1,r]建成凸包,再用[l,mid-1]中的点g计算答案 top=0; s[++top]=r+1; for(int i=r; i>mid+1; i--) { while(top>1&&gslope(s[top-1],s[top])<=gk(i)) top--; while(top>1&&gslope(s[top],i)>=gslope(s[top-1],s[top])) top--; s[++top]=i; } for(int i=mid; i>=l; i--) { while(top>1&&gslope(s[top-1],s[top])<=gk(i)) top--; tmp[i]=gc(i)-gx(s[top])*gk(i)+gy(s[top])+f[i-1]; } maxt=-INF; for(int i=l; i<=mid; i++) { maxt=max(maxt,tmp[i]); h[i]=max(h[i],maxt); } } int main() { int p; ll x; scanf("%d",&n); for(int i=1; i<=n; i++) { scanf("%lld",&a[i]); sum[i]=sum[i-1]+a[i]; } pre_dp(); solve(1,n); scanf("%d",&m); for(int i=1; i<=m; i++) { scanf("%d %lld",&p,&x); ll ans=-INF; ans=max(ans,f[p-1]+g[p+1]);//不选p ans=max(ans,h[p]-2*(x-a[p])); printf("%lld\n",ans/2);//我们dp方程整体乘了2 } }
[NAIPC2016]Jewel Thief(法1)
有个物品,每个物品有一个体积和价值,现在要求对,求出体积为的
背包能够装下的最大价值
决策单调性发现
注意到物品的体积很小,考虑按体积分类,选取同种体积的物品时,一定优先选择价值大的物品。
设为使用前i种体积的物品,体积为j的最大价值。类似多重背包的单调队列优化,将模i同余的所有位置拿出来重新标号(即下标看作1,2,3....x)。
则有
其中表示第种体积的物品中,最大的个的价值和
,的大小只与有关。且随着这个差值的增加,的增长速度会越来越慢(其实由于导数单调递减,是凹函数)。显然.容易发现,和一定的情况下,两个较大差值的加起来比一个小的加一个大的更大。也就是说。这与四边形不等式恰好相反。
定理1.2:对于形如的状态转移方程,若函数满足四边形不等式,则具有决策单调性
考虑之前对该定理的证明,把不等号反向,换成,就能证明决策单调性。
问题转化
令为一个矩阵,,即使得第列上最大值所在的行(如果有多个i相同,则取编号最大的)
在原问题中,考虑第层到第层的转移 ,令,我们发现dp的转移实际上就是在求第行第列的最大值,其中固定。那么
于是问题就转化为:已知一个大小的矩阵,其中每个元素的值均可以在时间内查询。现
要求对于,求出
分治求解
对列[l,r]进行分治,维护当前可能成为的行,令,暴力枚举所有可能的行求出,分治递归操作以及直至或。容易发现这样的时间复杂度是
#include<iostream> #include<cstdio> #include<cstring> #include<vector> #include<algorithm> #define maxw 300 #define maxn 1000000 using namespace std; typedef long long ll; vector<ll>w[maxw+5]; ll dp[2][maxn+5]; int n,m; void divide(int l,int r,int x,int y,int now,int mod,int rest){ //列[l,r],行[x,y]; if(l>r) return; int mid=(l+r)>>1,pos=mid; dp[now^1][mid*mod+rest]=dp[now][mid*mod+rest]; for(int j=min(y,mid-1);j>=x;j--){//枚举可能成为pos(mid)的列,注意j<mid if(mid-j>(int)w[mod].size()) break; if(dp[now][j*mod+rest]+w[mod][mid-j-1]>dp[now^1][mid*mod+rest]){ dp[now^1][mid*mod+rest]=dp[now][j*mod+rest]+w[mod][mid-j-1]; pos=j; } } divide(l,mid-1,x,pos,now,mod,rest); divide(mid+1,r,pos,y,now,mod,rest); } inline int cmp(int x,int y){ return x>y; } int main(){ int x,y; scanf("%d %d",&n,&m); for(int i=1;i<=n;i++){ scanf("%d %d",&x,&y); w[x].push_back(y); } for(int i=1;i<=maxw;i++){ sort(w[i].begin(),w[i].end(),cmp); for(int j=1;j<(int)w[i].size();j++) w[i][j]+=w[i][j-1]; } int now=0; for(int i=1;i<=300;i++){ if(w[i].size()){ for(int j=0;j<i;j++){ //将模i同余的所有位置拿出来 divide(0,(m-j)/i,0,(m-j)/i,now,i,j); } for(int j=1;j<=m;j++){ dp[now^1][j]=max(dp[now^1][j],dp[now^1][j-1]); //我们dp的子状态是体积<=j,而分治过程中是=j } now^=1; } } for(int i=1;i<=m;i++) printf("%I64d ",dp[now][i]); }
决策单调性的特殊解法: SMAWK算法
定义3.1 若矩阵满足则称为单调矩阵。若A的任意子矩阵均为单调矩阵,则称为完全单调矩阵
注:这里的子矩阵指的是一个矩阵去掉一些行和列后得到的矩阵,原矩阵里这些元素不一定相邻.
引理3.1 为完全单调矩阵,当且仅当为单调矩阵。这里表示的是把第行的列拿出来形成的子矩阵
用非形式化的语言说就是,一个矩阵为完全单调矩阵,当且仅当任意它“连续”(子矩阵中的每个元素在原矩阵中相邻)的子矩阵是单调矩阵。
证明:
必要性:由定义3.1可得,完全单调矩阵的任意一个子矩阵都是单调矩阵,那么任意"连续“的子矩阵也是。
充分性: 运用数学归纳法,对于,假设所有不包含它自身的子矩阵都是单调矩阵
由(论文中此处有误),得
由(论文中此处有误),得
把两个不等式结合到一起,就得到,由定义知,原矩阵是单调矩阵。根据假设,原矩阵是完全单调矩阵。而的情况容易证明。
用SMAWK的reduce()减少无用决策
reduce()的过程
在之前的分治算法中, 我们维护了可能成为当前所有列的最优答案的行区间。 有时,
行区间内的决策数将远远大于列数, 即每行内的决策大多数都是无用决策,
给出一个M行N列的完全单调矩阵A, 每个位置都可以询问。 要求求出任意
一个大小为N的行集合S, 使得
也就是说,S包含了每一列的最大值所在行。
我们使用SMAWK算法中的reduce
函数来解决这个问题,伪代码如下
void reduce(A,N,M){ for(int i=1;i<=M;i++) S[i]=i; p=1; while(S.size()>N){ if(A[S[p]][p]>A[S[p+1]][p]&&p<N) p++;//情况1 else if(A[S[p]][p]>A[S[p+1]][p]&&p==N) S.erase(S[p+1]);//情况2 else{ S.erase(S[p]);//情况3 p--; } } }
正确性证明
下面我们来证明这个算法的正确性:
在Reduce函数中, 我们使用一个数据结构S来储存每列可能成为答案的行。支持删除任意一个位置的值,并将后面的元素向前平移1位。还支持查询任意位置的值。(可以把它当成一个STLvector<int>
.至于S的删除为什么不会导致复杂度退化,见下方的时间复杂度分析。
初始时, 我们
假定就是第p列对应取到极值的行, 而的所有位置存储的都是备选决策。
定义3.2 若,则称为无用元素
引理3.2设为一个完全单调矩阵,若对,有,则为无用元素。反之,若,则为无用元素。
证明:
考虑子矩阵,由于是完全单调矩阵,根据引理3.1,这个子矩阵是单调矩阵。又因为,显然这个子矩阵的,而根据单调性,,即.所以是无用元素。
若,根据引理3.2,它在之前不是最优决策。又因为在p处也不如优,显然这个决策没有任何用,可以直接把这个决策删去,然后将后面的决策向左平移一位。
由于p的决策更换了,我们还需要继续比较前一个。对应到伪代码里,就是这一段:
else{ S.erase(S[p]);//删掉无用决策p p--;//将后面的决策向左平移一位,重新比较 }
否则,我们暂时认为是p的决策,然后继续去判定下一列。
if(A[S[p]][p]>A[S[p-1]][p]&&p<N) p++;//继续去判定下一列
若,也就是说在第列处都不能变优,那么直接删掉这个决策就可以了。
else if(A[S[p]][p]>A[S[p+1]][p]&&p==N) S.erase(S[p+1]);//直接删掉这个决策就可以了。
于是,我们就证明了SMAWK算法的正确性,再回顾一下整个过程
void reduce(A,N,M){ for(int i=1;i<=M;i++) S[i]=i; p=1; while(S.size()>N){ if(A[S[p]][p]>A[S[p-1]][p]&&p<N) p++;//继续去判定下一列 else if(A[S[p]][p]>A[S[p+1]][p]&&p==N) S.erase(S[p+1]);//直接删掉这个决策就可以了。 else{ S.erase(S[p]);//删掉无用决策p p--;//将后面的决策向左平移一位,重新比较 } } }
注意我们求出了S,只是保证,顺序不一定一样。并不是第列的真正决策行。我们所能够知道的,仅仅是列
复杂度证明
该算法的时间复杂度主要消耗在的移动和删除元素
注意到每减少1,必然伴随着的删除。也就是说的每一次抵消都可以视为花费2的
代价使得。
也就是的减少到至多需要花费(我们规定N,M同阶).再加上指针移动的,总的复杂度是的
真正的SMAWK算法
回到我们之前提到的分治法
对列[l,r]进行分治,维护当前可能成为的行,令,暴力枚举所有可能的行求出,分治递归操作以及直至或。容易发现这样的时间复杂度是
我们只是把 "暴力枚举所有可能的行求出" 这一段优化到了,总时间复杂度还是 。一种更好的解法是每次将所有奇数位的行取出来进行递归,计算出所有奇数位的决策后,再利用决策单调性的性质求出偶数位的决策。
void SMAWK(A){ reduce(A,N,M); for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ if(j%2==1) B[i][j/2]=A[i][j]; } } SMAWK(B); 用pos(2i-1)和pos(2i+1)求出pos(2i); }
设为计算一个的矩阵的所有pos的时间复杂度,有
由于reduce()之后行数和列数都变成相同,总时间复杂度为
[NAIPC2016]Jewel Thief(法2)
我们将实现留给读者
[UOJ 285]数据分块鸡(法2)
由于SMAWK的限制较多,此题还需要一些转化才能使用SMAWK算法。
鉴于SMAWK在此题的实际运行时间相比单调队列并不优秀,且可扩展性较差,我们在这里不详细展开,而是讲解一些更实用的决策单调性技巧
番外:
另类决策单调性:区间dp的四边形不等式优化
在区间dp中,我们常常遇到下面这样的状态转移方程:
在这一节中,我们将把一维的决策单调性推广到二维
定理5.1 在状态转移方程中(特别地,),如果下面两个条件成立:
- 满足四边形不等式
- ,有$ w(a,d) \geq w(b,c) f$也满足四边形不等式。
证明:
当时,我们有:
若的最优决策是,那么有
因此
即,满足四边形不等式
当决策为时同理。
接下来用数学归纳法,假设时,满足四边形不等式。考虑的情况,设的最优决策为,的最优决策是,不妨设
对于根据的最优性有:
对于,和不一定最优
根据归纳假设,有
因为满足四边形不等式,有
,得
代入,有
四边形不等式成立。证毕。
定理5.2 在状态转移方程中(特别地,),设为取到最小值时的值
如果满足四边形不等式,那么
证明:
设,对于,因为满足四边形不等式,有:
根据的最优性,有:
,得:
容易看出这是的转移,也就是说,对于,选转移比选小于的任意决策更优。所以的最优决策一定在及以后取到。所以
同理可证
那么,当满足四边形不等式和区间包含关系单调,我们就可以利用上述两个定理优化DP.这种优化不需要任何数据结构。转移时照常枚举,而只需要从枚举到
总复杂度为:
类似尺取法的复杂度分析,总的复杂度不是而是
[NOI1995]石子合并(加强版)
有堆石子排成一个环,每堆石子有个。一开始每个石子为单独一堆。可以把相邻的两堆石子合并为一堆,合并的代价为两堆石子的个数之和,求把所有石子合并成一堆的最小代价和最大代价。
首先把环复制一遍,断环为链。
设为的区间和。容易写出两个dp方程:
显然满足四边形不等式,因此有决策单调性,可以用上面提到的方法求出。
贪心考虑,的最优转移一定是或,因为这样合并最不“均衡”,可以让大的被合并很多次。严格证明可以用反证法。
另外,此题有的非动态规划算法,这超出了我们讨论的范围。
#include<iostream> #include<cstdio> #include<cstring> #define maxn 1000 #define INF 0x3f3f3f3f using namespace std; int n; int a[maxn+5]; int sum[maxn+5]; int f[maxn+5][maxn+5]; int p[maxn+5][maxn+5]; int g[maxn+5][maxn+5]; inline int w(int x,int y){ return sum[y]-sum[x-1]; } int main(){ scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); a[i+n]=a[i]; } for(int i=1;i<=n*2;i++) sum[i]=sum[i-1]+a[i]; memset(f,0x3f,sizeof(f)); for(int i=1;i<=n*2;i++){ p[i][i]=i; f[i][i]=0; } for(int len=2;len<=n*2;len++){ for(int i=1;i+len-1<=n*2;i++){ int j=i+len-1; for(int k=p[i][j-1];k<=p[i+1][j];k++){ if(f[i][k]+f[k+1][j]+w(i,j)<f[i][j]){ f[i][j]=f[i][k]+f[k+1][j]+w(i,j); p[i][j]=k; } } g[i][j]=max(g[i][j-1],g[i+1][j])+w(i,j); } } int mins=INF,maxs=0; for(int i=1;i<=n;i++){ mins=min(mins,f[i][i+n-1]); maxs=max(maxs,g[i][i+n-1]); } printf("%d\n%d\n",mins,maxs); }
另类决策单调性:NOIP2019D2T2
在这一节中,我们抛开四边形不等式,用决策单调性最朴素的定义来解决问题题。
给出一个长度为的序列,把这个序列划分为几块,满足从前往后块内的权值和严格递增。求每个块权值的平方之和的最小值
记的前缀和为.设表示前个数,且最后一块的右端点为的最小值。表示取到最小值时最后一块的左端点(不包含),实际上就是的最优转移。
那么有:
直接转移是的,且没有什么优化空间. 容易发现,如果知道了所有的,那么的值可以根据推出,直接沿着数组往前跳即可。下面的代码就求出了.
int x=n; while(x>0){ ans+=(s[x]-s[g[x]])*(s[x]-s[g[x]]); x=g[x]; }
那么,是否有特殊的性质呢?
引理6.1: 在最优的划分方案中,最后一块的和应该尽量小。形式化的说, (越大,根据前缀和的单调性知区间和越小)
感性理解,,所以把大段拆分成小段更优。严谨证明如下:
当,满足条件的至多有一个,结论显然成立
假设 时结论成立,现在我们要证明时结论也成立,用反证法。
若时结论不成立,令那么一定存在 使得
展开并移项,有
对右边因式分解,有
又,所以
左边加上,有
代入,有,即. 也就是说,把分为一段会得到一个更小的,这与的最小性矛盾。因此原命题成立。
根据数学归纳法,原命题恒成立。
根据引理,我们发现这题的决策单调性不仅是简单的单调递增,并且还满足一些特殊的性质。那么就可以根据这些性质快速求出决策。
把条件转换一下,变成$ s[i]>2s[j]-s[g[j]] s[i]$也单调递增,维护一个按照递增的单调队列即可。
inline ll calc(int x){ return s[x]*2-s[g[x]]; } for(int i=1;i<=n;i++){ while(head<tail&&calc(q[head+1])<=s[i]) head++;//满足条件的只留最大的一个 g[i]=q[head]; while(head<tail&&calc(q[tail])>=calc(i)) tail--;//保持递增 q[++tail]=i; }
最后3个测试点需要手写高精度,这里偷懒用了__int128
#include<iostream> #include<cstdio> #include<cstring> #define maxn 40000000 #define maxm 100000 using namespace std; typedef long long ll; typedef __int128 bignum; //int128真香 template<typename T> inline void qread(T &x){//template真香 x=0; T sign=1; char c=getchar(); while(c<'0'||c>'9'){ if(c=='-') sign=-1; c=getchar(); } while(c>='0'&&c<='9'){ x=x*10+c-'0'; c=getchar(); } x=x*sign; } void qprint(bignum x){ if(x<0){ putchar('-'); qprint(-x); }else if(x==0){ putchar('0'); return; }else{ if(x>=10) qprint(x/10); putchar('0'+x%10); } } int n,type; ll s[maxn+5]; void gen(){ static ll l[maxm+5],r[maxm+5],p[maxm+5]; static ll b[3];//滚动数组卡内存 ll x,y,z; int m; qread(x); qread(y); qread(z); qread(b[0]); qread(b[1]); qread(m); for(int i=1;i<=m;i++){ qread(p[i]); qread(l[i]); qread(r[i]); } int j=0; int cur=2; for(int i=1;i<=n;i++){ while(p[j]<i) j++; if(i<=2) s[i]=(b[i-1]%(r[j]-l[j]+1)+l[j]); else{ b[cur]=(x*b[(cur-1+3)%3]+y*b[(cur-2+3)%3]+z)%(1<<30); s[i]=(b[cur]%(r[j]-l[j]+1)+l[j]); cur=(cur+1)%3; } } } int g[maxn+5]; int q[maxn+5]; inline ll calc(int x){ return s[x]*2-s[g[x]]; //s[i]-s[j]>=s[j]-s[g[j]],化成s[i]>=2*s[j]-s[g[j]] } int main(){ qread(n); qread(type); if(type==0) for(int i=1;i<=n;i++) qread(s[i]); else gen(); for(int i=1;i<=n;i++) s[i]+=s[i-1]; int head=1,tail=1; for(int i=1;i<=n;i++){ while(head<tail&&calc(q[head+1])<=s[i]) head++; g[i]=q[head]; while(head<tail&&calc(q[tail])>=calc(i)) tail--;//把最后一段和大的弹出去 q[++tail]=i; } bignum ans=0; int x=n; while(x>0){ ans+=(bignum)(s[x]-s[g[x]])*(s[x]-s[g[x]]); x=g[x]; } qprint(ans); }
总结
我们在本文中一共提到了决策单调性的四种解法:
- 单调队列+二分查找
- 斜率优化
- 分治
- SMAWK算法
文中提到的算法各有优劣
线性的斜率优化算法在所有算法中是时间复杂度最优秀的算法,但在使用时的局限性也最大。当函数的变化比较平缓时,决策单调性中不同的决策点比较少,此时分治法与二分法的询问次
数较少,有时在随机数据下能够做到期望线性。而当函数比较陡峭(如NOI2009《诗人小G》
)时,队列中元素较多,二分算法的询问次数就是接近的。而线性算法的询问次
数则比较稳定,这也启示我们应该根据题目选择更为合适的算法来解题。
实际上,一般的题目用单调队列+二分查找和分治法就足够了,即使时间复杂度上稍有不足,但适当的常数优化可以提高程序的执行效率。因此,我们建议读者熟练掌握以上两种方法。
参考资料:
冯哲《浅谈决策单调性动态规划的线性解法》
毛子青《动态规划算法的优化技巧》
李煜东《算法竞赛进阶指南》
UOJ 数据分块鸡 官方题解
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】