2019 年,第一届 CSP 认证的考场上,作为选手的asuldb打开了第二题。经过一番观察,他认为这道题存在决策单调性,于是开始乱写最终发现过不了样例。

最终asuldb对该题设计出了一个暴力程序,对于一组规模为\(u\)的数据,该程序的运行时间极有可能是\(u^2\),之后又由于asuldb写错了输出,他只获得了60pts的好成绩。

之后他得知这道题只是一个普通的单调队列优化,于是他自闭了。

于是自闭的asuldb前几天决定来学一学决策单调性,看看决策单调性到底是个什么东西

大致可以分成两种策略

一、分治

主要用于优化满足决策点单调,且同一层dp值无关的问题,即\(dp_i=\max_{j<i}f_j+w_{j+1,i}\)这类的dp,需要满足四边形不等式

四边形不等式:对于\(a\leq b\leq c\leq d\),如果满足\(w_{b,c}+w_{a,d}\leq w_{a,c}+w_{b,d}\)那么就会存在决策点单调可以使用分治的策略优化

具体的分治方法就是我们对于一个区间\([l,r]\)我们取其中点\(mid\)在相应的决策点区间\([x,y]\)找到最优的决策点\(k\),那么\([l,mid-1]\)的可能决策点区间就是\([x,k]\),\([mid+1,y]\)的可能决策点区间就是\([k,y]\);分治这两个区间就好了,复杂度显示是\(O(n\log n)\)的

一道经典的例题​​cf868f​

首先来证明一下为什么会满足四边形不等式,我们找到一组\(a\leq b\leq c\),使得\(d\)从\(c\)开始递增,对于\(w_{b,c}+w_{a,d}\),\(d\)每增加\(1\)产生的贡献是\([b,c],[a,d-1]\)里和\(d\)相同的数的个数;这个数显然要少于\([a,c],[b,d-1]\)里和\(d\)相同的个数,于是有决策单调性,我们可以使用分治的策略解决这个问题

代码

#include<bits/stdc++.h>
#define re register
#define LL long long
const int maxn=1e5+5;
inline int read() {
char c=getchar();int x=0;while(c<'0'||c>'9')c=getchar();
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-48,c=getchar();return x;
}
int n,m,L,R,a[maxn],tax[maxn];
LL g[maxn],dp[maxn],nw;
inline void add(int c,int v) {
nw-=1ll*tax[c]*(tax[c]-1)/2ll;
tax[c]+=v;
nw+=1ll*tax[c]*(tax[c]-1)/2ll;
}
inline LL calc(int l,int r) {
while(R<r)add(a[++R],1);
while(L>l)add(a[--L],1);
while(R>r)add(a[R--],-1);
while(L<l)add(a[L++],-1);
return nw;
}
void solve(int l,int r,int x,int y) {
if(l>r) return;
int mid=l+r>>1;dp[mid]=1e18;int id=0;
for(re int i=x;i<=y&&i<mid;i++) {
LL k=g[i]+calc(i+1,mid);
if(k<dp[mid]) dp[mid]=k,id=i;
}
if(l==r) return;
solve(l,mid-1,x,id);solve(mid+1,r,id,y);
}
int main() {
n=read(),m=read()-1;
for(re int i=1;i<=n;i++) a[i]=read();
L=R=1;tax[a[1]]=1;
for(re int i=1;i<=n;i++) dp[i]=calc(1,i);
while(m--) {
for(re int i=1;i<=n;i++)g[i]=dp[i];
solve(1,n,1,n);
}
printf("%lld\n",dp[n]);
return 0;
}


二、二分单调栈

主要用于优化\(dp_i=\max_{j<i}dp_j+w_{j+1,i}\)类的转移,是一种更为广义的斜率优化

一道例题​​[JSOI2011]柠檬​

首先观察到我们划分出来的区间首尾肯定是同色的,而且首尾的颜色就是我们选择的颜色;如果首位不是我们所选择的颜色,我们完全可以选一个更小的区间使得贡献不变。

于是可以写出这样的方程\(dp_{i}=\max_{j\leq i,a_j=a_i}dp_{j-1}+a_i(s_i-s_j+1)^2\),其中\(s_i=\sum_{j=1}^i[a_j=a_i]\)

我们思考一下其中的决策单调性,对于\(i\)这个位置,如果存在\(j,k,j<k\)这样的两个转移,这个时候\(j\)转移已经要优于\(k\)转移了,那么对于比\(i\)更大的位置,\(j\)转移还是要优于\(k\),因为\(j\)的增长速度要快于\(k\)

于是我们可以维护一个存储决策点的栈,设\(f(j,k)\)表示决策\(j\)最早在什么时候好于决策\(k\),这个可以在一个\(\log\)时间内二分求出,那么这个栈中任意三个连续元素\(j,k,l\)肯定会满足\(f(j,k)>f(k,l)\);否则\(k\)这个决策点就太没用了,在\(k\)比\(l\)优的时候已经有一个比\(k\)更优的了,这样\(k\)永远也不会是最优决策了,对于这样的\(k\)我们不应该留在栈中

于是这道题我们对于每一种颜色维护一个单调栈就好了,复杂度\(O(n\log n)\)

#include<bits/stdc++.h>
#define re register
#define LL long long
#define lst(x) stk[x][stk[x].size()-1]
#define Lst(x) stk[x][stk[x].size()-2]
inline int read() {
char c=getchar();int x=0;while(c<'0'||c>'9')c=getchar();
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-48,c=getchar();return x;
}
const int maxn=1e5+5;
int a[maxn],p[maxn],tax[10005],n;
LL dp[maxn];
std::vector<int> stk[10005];
inline LL trans(int x,int c) {
return dp[x-1]+1ll*(c-p[x]+1)*(c-p[x]+1)*a[x];
}
inline int chk(int a,int b) {
int l=p[a]>p[b]?p[a]:p[b],r=n,nw=n+1;
while(l<=r) {
int mid=l+r>>1;
if(trans(a,mid)>=trans(b,mid)) r=mid-1,nw=mid;else l=mid+1;
}
return nw;
}
int main() {
n=read();for(re int i=1;i<=n;i++)a[i]=read(),p[i]=++tax[a[i]];
for(re int i=1;i<=n;i++) {
re int c=a[i];
while(stk[c].size()>=2&&chk(Lst(c),lst(c))<=chk(lst(c),i)) stk[c].pop_back();
//维护单调栈的性质
stk[c].push_back(i);
while(stk[c].size()>=2&&chk(Lst(c),lst(c))<=p[i]) stk[c].pop_back();
//如果栈尾在当前不是最优的,那么在以后肯定也不是,于是弹栈
dp[i]=trans(lst(c),p[i]);
}
printf("%lld\n",dp[n]);return 0;
}