决策单调性优化dp学习笔记

决策单调性优化dp学习笔记

@

决策单调性的定义

四边形不等式

定义1.1.1:

若函数\(w(x,y)(\mathbb{Z} \times \mathbb{Z} \rightarrow \mathbb{Z})\)对于\(\forall a,b,c,d \in \mathbb{Z}\),其中\(a \leq b \leq c \leq d\),都有\(w(a,d)+w(b,c) \geq w(a,c)+w(b,d)\),则称函数\(w\)满足四边形不等式

如果我们考虑用图形来表达,那么可以简记为“交叉”和“包含”的关系

这是四边形不等式最基本的定义。但是在做题中我们常常遇到下面的一种形式.

推论1.1.1:

若函数\(w(x,y)(\mathbb{Z} \times \mathbb{Z} \rightarrow \mathbb{Z})\)对于\(\forall a,b \in \mathbb{Z}\),其中\(a<b\),都有\(w(a,b+1)+w(a+1,b) \geq w(a,b)+w(a+1,b+1)\),则称函数\(w\)满足四边形不等式

证明:

对于\(a<c\),有:\(w(a,c+1)+w(a+1,c)≥w(a,c)+w(a+1,c+1)\)

对于\(a+1<c\),有:\(w(a+1,c+1)+w(a+2,c)≥w(a+1,c)+w(a+2,c+1)\)

两式相加,得:\(w(a,c+1)+w(a+1,c)+w(a+1,c+1)+w(a+2,c)≥w(a,c)+w(a+1,c+1)+w(a+1,c)+w(a+2,c+1)\)

整理得:
\(w(a,c+1)+w(a+2,c)≥w(a,c)+w(a+2,c+1)\)

以此类推,可得

\(\forall a \leq b \leq c,\ \ w(a,c+1)+w(b,c)\geq w(a,c)+w(b,c+1)\)

同理对第二个数做这样的证明,即可得到

\(\forall a \leq b \leq c \leq d, \ \ w(a,d)+w(b,c) \geq w(a,c)+w(b,d)\)

另外,如果函数\(w\)满足四边形不等式,我们也称它满足凸完全单调性,或者说它是凸函数。关于函数凹凸性的讨论超出了本文的讨论范围,这里不再讨论

四边形不等式与决策单调性

定义1.2.1:对于形如\(f[i]=\min _{0 \leq j < i}(f[j]+w(j,i))\)的状态转移方程,记\(p[i]\)\(f[i]\)取到最小值时\(j\)的值.\(p[i]\)即为\(f[i]\)的最优决策。如果\(p[i]\)\([1,n]\)上单调不减,则称\(f\)具有决策单调性

定理1.2:对于形如\(f[i]=\min _{0 \leq j < i}(f[j]+w(j,i))\)的状态转移方程,若函数\(w\)满足四边形不等式,则\(f\)具有决策单调性

\(\forall i\in [1,n],j \in [0,p[i]-1]\),根据决策单调性的定义得:

\(f[p[i]]+w(p[i],i) \leq f[j]+w(j,i) \ \ \ \ \ \ (1)\)(即\(p[i]\)前的决策都没有\(i\)优)

\(\forall i' \in[i+1,n]\),显然\(j<p[i]<i<i'\),由于\(w\)满足四边形不等式,那么有

\(w(j,i')+w(p[i],i) \geq w(j,i)+w(p[i],i')\)

移项,得\(w(p[i],i')-w(p[i],i) \leq w(j,i')-w(j,i) \ \ \ \ \ \ (2)\)

\((1)+(2)\)\(f[p[i]]+w(p[i],i') \leq f[j]+val(j,i')\)

那么对于\(i'\)来说,以\(p[i]\)作为\(i'\)的决策,比\(j<p[i]\)作为\(i'\)的决策更优。因此\(f[i'](i'>i)\)的最优决策不可能小于\(p[i]\),即\(p[i'] \geq p[i]\).\(f\)有决策单调性

于是,我们得到了证明一个dp方程满足决策单调性的方法:

  1. 证明权函数\(w\)满足四边形不等式(比赛中可以采用打表估(xia)计(cai)的方法)
  2. 根据定理1.2,状态转移方程\(f\)满足决策单调性

然而这样有什么用呢?根据决策单调性,我们可以把原来的\(O(n^2)\)的dp优化到\(O(n\log n)\)甚至是\(O(n)\)

决策单调性的通用解法:单调队列+二分查找

[BZOJ 1563] [NOI 2009] 诗人小G

题目链接

一首诗包含了若干个句子,对于一些连续的短句,可以将它们用空格隔开并放在一行中,注意一行中可以放的句子数目是没有限制的。小 G 给每首诗定义了一个行标准长度(行的长度为一行中符号的总个数),他希望排版后每行的长度都和行标准长度相差不远。显然排版时,不应改变原有的句子顺序,并且小 G 不允许把一个句子分在两行或者更多的行内。在满足上面两个条件的情况下,小 G 对于排版中的每行定义了一个不协调度, 为这行的实际长度与行标准长度差值绝对值的 P 次方,而一个排版的不协调度为所有行不协调度的总和。

小 G 最近又作了几首诗,现在请你对这首诗进行排版,使得排版后的诗尽量协调(即不协调度尽量小),并把排版的结果告诉他

\(n \leq 5\times 10^5\)

转移方程推导

显然,设\(dp[i]\)为选了第\(i\)个句子并在此换行的最小不协调度。每句诗的长度为\(a[i]\),\(sum[i]\)为前\(i\)句诗的总长度,那么

\[dp[i]=\min_{0 \leq j <i}(dp[j]+|sum[i|-sum[j]+(i-j-1)-L|^P) \]

后面的式子表示把第\((j,i]\)句分成一行的代价。句子长度为\(sum[i]-sum[j]\),空格长度为\(i-j-1\)

这里的\(w\)函数为\(w(j,i)=|sum[i|-sum[j]+(i-j-1)-L|^P\),由于\(P\)的次数较高,无法斜率优化。于是尝试证明\(w\)满足四边形不等式

决策单调性证明

我们要证明\(\forall j<i,w(j,i+1)+w(j+1,i) \geq w(j,i)+w(j+1,i+1)\)

移项,得\(w(j+1,i)-w(j+1,i+1) \geq w(j,i)-w(j,i+1)\)

记:

\(u=(sum[i]+i)-(sum[j]+j)-(L+1)\)

\(v=(sum[i]+i)-(sum[j+1]+j+1)-(L+1)\)

则只需证明

\[|v|^P=|v+(a[i+1|+1)|^P \geq |u|^P -|u+(a[i+1]+1)|^P \]

即证明对于任意常数\(c\),函数\(h(x)=|x|^P-|x+c|^P\)单调递减.证明比较繁琐,这里引用一下

prof1.JPG

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1L1uLFbE-1578552752630)(https://i.loli.net/2019/12/16/wLXR9QsjvOgUkF8.jpg)]

总之,\(w\)满足四边形不等式,那么\(f\)有决策单调性

优化方法

由于单调性,每个决策\(x\)肯定存在一个区间\([l[x],r[x]]\)使得当前情况下\(p[k]=x(k \in[l[x],r[x]])\)

\(pos(x,y)\)表示当前情况下,第一个以\(x\)为决策点不如以\(y\)为决策点更优的位置(如果当前只计算到\(dp[i]\),则对于\(i'>i\),\(p[i']=i\))。则\(r[x]=l[y]=pos(x,y)\).\(pos\)可以二分查找求出。

我们维护一个单调队列存储决策点。在处理\(dp[i]\)时,我们这样做:

  1. 如果队头的决策点对应区间不包含i,即\(r[q[head]]=pos(q[head],q[head+1])<i\)则出队

  2. 通过队头决策点转移

  3. 通过二分寻找出最左边的,以\(q[tail]\)为决策点不如以i为决策点更优的位置。这个位置实际上是\(l[i]\).由于决策单调性,目前从这个位置往右的 dp 都满足以i为决策点是最优的。再二分出\(l[q[tail]]=pos(q[tail-1],q[tail])\),如果\(l[i]<r[q[tail]]\),说明\(q[tail]\)决策点对应的所有转移都不如\(i\)更优,我们把\(q[tail]\)出队,继续比较下一个决策点

  4. 当队尾的弹出停止的时候,将\(i\)入队,且\(i\)对应区间右端点为\(n\)

#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)

题目链接

给出一个长度为\(n-1\)的序列,编号为\([1,n)\).还有\(m\)个询问\([l,r)\),表示求\([l,r)\)中的元素和。现在要利用分块的思想,可以选择任意位置作为分割点把序列分为多块。询问时一个块的代价是1.对于不足一个块的部分,若这个块中需要求和的区间小于这个块大小的一半,则代价为该区间长度。否则可以用块的总和相减,代价为块大小-该区间长度。

求如何分块使得所有询问的代价之和最小

转移方程推导

我们考虑每个块对答案的贡献,设\(val(j,i)\)表示把\([j,i)\)分为一块对答案的贡献。设\(dp[i]\)表示已经分好\([1,i]\),最后一个分割点在\(i\)的最小代价,那么显然有:

\[dp[i]=\max(dp[j]+val(j+1,i)) \]

这个方程的形式和上一题几乎一样。容(da)易(dan)证(cai)明(xiang)\(val\)满足四边形不等式,因此该方程满足决策单调性,可以直接套用上一题的方法求解。

那么如何求\(val\)?可以大力分类讨论询问区间\([x,y]\)与分块区间的关系(相交、包含),一共有六种情况(实际上原题题面已经指明了分类讨论的方向,具体方法见下面代码)

//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函数相当于一个二维前缀和。由于数据范围较大,可以用可持久化线段树或二维线段树实现。详见代码。

另外由于可持久化线段树的查询,该方法的的复杂度是\(O(n \log^2n)\)。因此要格外注意常数优化。一个很强的优化是,对于队列中的每个节点,我们把它作为最优决策的区间存储下来,这样就不需要出入队的时候再二分去计算。

#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]);
}

决策单调性的特殊解法:斜率优化

先简单复习一下斜率优化:

斜率优化是决策单调性问题一种衍生算法,一类特殊的决策单调性问题可以利用斜率
优化在线性时间内得到解决。

将一个决策 \(j\)看作平面上的一个点\((x_j,y_j)\)。对于i来说,\(\forall k<j,f_j+w(j,i) \leq f_k+w(k,j)\),当且仅当\(\frac{y_j-y_k}{x_j-x_k} \leq C_i\)

其中\(x_j,y_j\)均在计算\(f_j\)后已知,\(C_i\)已知

实现这一算法的一般方法为维护所有决策点构成的凸包

  1. \(x,C\)单调递增时可以利用单调队列直接维护凸包,询问时只需弹出队首不满足条件的元素即可。 时间复杂度\(O(n)\)
  2. \(x\)单调递增时,在凸包上二分寻找第一个斜率不超过\(C_i\)的位置,时间复杂度\(O(n \log n)\)
  3. \(x,C\)均不单调递增时可以使用cdq分治.时间复杂度\(O(n \log n)\)

[ARC 66D]Contest with Drinks Hard(弱化版)

题目链接

在一场比赛中一共有\(n\)道题,其中解决第\(i\)道题需要花费\(T_i\)秒。你可以在所有题中挑选
任意一些题并解决它们。定义一种解决方案所得的分数为满足\(\forall i \in [L,R]\)\(i\)道题被解决
\(1 ≤ L ≤ R ≤ n\)\([L,R]\)对数减去解决问题所需要的时间和。
求最高分数

\(n \leq 3 \times 10^5\)

斜率优化推导

\(sum\)为序列\(a\)的前缀和,\(dp[i]\)表示对于前i个位置其中第i个不选的最大价值,假设枚举j表示上一个不选的位置是j。那么\([j+1,i]\)成为了一个满足题面条件的区间,增加的对数为\(C_{i-j}^2=\frac{(i-j)(i-j-1)}{2}\),那么

\[dp[i]=\min_{0 \leq j <i},dp[j]-(sum[i-1]-sum[j])+\frac{(i-j)(i-j-1)}{2} \]

化成\(dp[j]+sum[j]+\frac{j^2+j}{2}=ji+dp[i]+sum[i-1]-\frac{i^2-i}{2}\)

决策点为\((j,dp[j]+sum[j]+\frac{j^2+j}{2})\),斜率为\(i\),最小化截距\(dp[i]+sum[i-1]-\frac{i^2-i}{2}\),可以直接斜率优化

决策单调性的特殊解法:整体分治

[ARC 66D]Contest with Drinks Hard

题目链接

在一场比赛中一共有\(n\)道题,其中解决第\(i\)道题需要花费\(T_i\)秒。你可以在所有题中挑选
任意一些题并解决它们。定义一种解决方案所得的分数为满足\(\forall i \in [L,R]\)\(i\)道题被解决
\(1 ≤ L ≤ R ≤ n\)\([L,R]\)对数减去解决问题所需要的时间和。
现给出\(m\)组询问,第i组询问需要你求出将第\(X_i\)道题的解决时间变为\(Y_i\)秒后,分数最高
方案的得分(询问后序列还原)。

\(n \leq 3 \times 10^5\)

朴素情况的斜率优化见上

带修改的情况

考虑将pos位置改为x,那么原来要么选了\(a_{pos}\),要么没有选\(a_{pos}\)

  1. 不选\(a_{pos}\),那么只需要预处理出前缀和后缀的dp值\(f\)\(g\),方程类似朴素情况,那么答案就是\(f[pos]+g[pos]\)
  2. 选了\(a_{pos}\),那么要满足选的数构成的区间包含pos。考虑预处理出\(h\)表示强制选第\(pos\)个的dp值,那么答案就是\(h[pos]+a[pos]-x\)

考虑如何求\(h\).直接枚举跨越\(i\)的区间比较难办,可以分治,每个分治区间[l,r]只讨论:前缀意义下,选了上一个不选的在[l,mid],最后一个选的在[mid+1,r]的答案。以及后缀意义下上一个不选的在[mid+1,r],最后一个选的在[l,mid]的答案

具体的说,我们讨论前缀意义下的情况:类似cdq分治的斜率优化,先把\([l,mid]\)的点按\(f\)建出凸包,对于\([mid+1,r]\)的点\(p\),在凸包上查询截距最小值,求出对应的\(f\)之后再加上\(g[p+1]\),就是\(h[p]\)的一个可能值,用这个值去更新答案即可。后缀意义同理。
代码实现中需要注意边界(毒瘤!)。

#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)

题目链接

\(n\)个物品,每个物品有一个体积\(w_i\)和价值\(v_i\),现在要求对\(V \in [1,m]\),求出体积为\(V\)
背包能够装下的最大价值

\(1 ≤ n ≤ 1000000; 1 ≤ m ≤ 100000; 1 ≤ w_i ≤ 300; 1 ≤ v_i ≤ 10^9\)

决策单调性发现

注意到物品的体积很小,考虑按体积分类,选取同种体积的物品时,一定优先选择价值大的物品。

\(dp[i][j]\)为使用前i种体积的物品,体积为j的最大价值。类似多重背包的单调队列优化,将模i同余的所有位置拿出来重新标号(即下标看作1,2,3....x)。
则有

\[dp[i][j]=\max_{k=0}^j dp[i-1][k]+val(k,j) \]

其中\(val(k,j)\)表示第\(i\)种体积的物品中,最大的\(j-k\)个的价值和

,\(val(k,j)\)的大小只与\(j-k\)有关。且随着这个差值的增加,\(val\)的增长速度会越来越慢(其实由于导数单调递减,是凹函数)。显然\((d-a)+(c-b)=(c-a)+(d-b)\).容易发现,和一定的情况下,两个较大差值的加起来比一个小的加一个大的更大。也就是说\(val(a,d)+val(b,c) \leq val(a,c)+val(b,d)\)。这与四边形不等式恰好相反。

定理1.2:对于形如\(f[i]=\min _{0 \leq j < i}(f[j]+w(j,i))\)的状态转移方程,若函数\(w\)满足四边形不等式,则\(f\)具有决策单调性

考虑之前对该定理的证明,把不等号反向,\(\min\)换成\(\max\),就能证明决策单调性。

问题转化

\(A\)为一个矩阵,\(pos(j)=max(\{i|,\forall p \in [0,m],A[i][j]>A[p][j] \})\),即使得第\(j\)列上最大值所在的行\(i\)(如果有多个i相同,则取编号最大的)

在原问题中,考虑第\(i-1\)层到第\(i\)层的转移 ,令\(A[k][j]=\begin{cases} dp[i-1][k]+val(k,j),k \leq j \\ -\infty,k>j\end{cases}\),我们发现dp的转移实际上就是在求第\(k\)行第\(j\)列的最大值,其中\(j\)固定。那么\(dp[i][j]=A[pos(j)][j]\)

于是问题就转化为:已知一个\((m+1)\times(m+1)\)大小的矩阵\(A\),其中每个元素的值均可以在\(O(1)\)时间内查询。现
要求对于\(j \in [0,m]\),求出\(A[pos(j)][j]\)

分治求解

对列[l,r]进行分治,维护当前可能成为\(pos\)的行\([x,y]\),令\(mid=\frac{l+r}{2}\),暴力枚举所有可能的行求出\(pos(mid)\),分治递归操作\([l,mid-1] [x,pos(mid)]\)以及\([mid +1,r] [pos(mid),y]\)直至\(l = r\)\(x = y\)。容易发现这样的时间复杂度是\(O(m\log m)\)

#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\)满足\(\forall i,j \in [0,k],pos(i)<pos(j)\)则称\(A\)为单调矩阵。若A的任意子矩阵均为单调矩阵,则称\(A\)为完全单调矩阵

注:这里的子矩阵指的是一个矩阵去掉一些行和列后得到的矩阵,原矩阵里这些元素不一定相邻.

引理3.1 \(A\)为完全单调矩阵,当且仅当\(\forall i_1<i_2,j_1<j_2,A[i_1,i_2:j_1 j_2]\)为单调矩阵。这里\(A[i_1,i_2:j_1,j_2]\)表示的是把第\(i_1 \sim i_2\)行的\(j_1 \sim j_2\)列拿出来形成的子矩阵

用非形式化的语言说就是,一个矩阵为完全单调矩阵,当且仅当任意它“连续”(子矩阵中的每个元素在原矩阵中相邻)的子矩阵是单调矩阵。

证明:

必要性:由定义3.1可得,完全单调矩阵的任意一个子矩阵都是单调矩阵,那么任意"连续“的子矩阵也是。

充分性: 运用数学归纳法,对于\(A[i_1,i_2,\dots i_k : j_1,j_2,\dots j_m](m \geq 3)\),假设所有不包含它自身的子矩阵都是单调矩阵

\(A[i_1,i_2,\dots i_k : j_1,j_2,\dots j_{m-1}]\)(论文中此处有误),得\(pos(j_1)\leq pos(j_2) \leq pos(j_m-1)\)

\(A[i_1,i_2,\dots i_k : j_2,j_3,\dots j_{m}]\)(论文中此处有误),得\(pos(j_2)\leq pos(j_3) \leq pos(j_m)\)

把两个不等式结合到一起,就得到\(pos(j_1) \leq pos(j_2 ) \leq pos(j_m)\),由定义知,原矩阵是单调矩阵。根据假设,原矩阵是完全单调矩阵。而\(m <3\)的情况容易证明。

用SMAWK的reduce()减少无用决策

reduce()的过程

在之前的分治算法中, 我们维护了可能成为当前所有列的最优答案的行区间。 有时,
行区间内的决策数将远远大于列数, 即每行内的决策大多数都是无用决策,

给出一个M行N列的完全单调矩阵A, 每个位置都可以\(O(1)\)询问。 要求求出任意
一个大小为N的行集合S, 使得\(\forall j \in [1,N],pos(j) \in 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来储存每列可能成为答案的行。\(S\)支持删除任意一个位置的值,并将后面的元素向前平移1位。还支持查询任意位置的值。(可以把它当成一个STLvector<int>.至于S的删除为什么不会导致复杂度退化,见下方的时间复杂度分析。

初始时, 我们
假定\(S_p\)就是第p列对应取到极值的行, 而\(S_i(i>N)\)的所有位置存储的都是备选决策。

定义3.2\(pos(j) \neq i\),则称\(A[i][j]\)为无用元素

引理3.2\(A\)为一个完全单调矩阵,若对\(i_1<i_2\),有\(A[i_1][j]>A[i_2][j]\),则\(A[i_2][c] (c \in [1,j])\)为无用元素。反之,若\(A[i_1][j] \leq A[i_2][j]\),则\(A[i_1][c] (c \in [j,n])\)为无用元素。

证明:

考虑子矩阵\(A[i_1,i_2:c,j]\),由于\(A\)是完全单调矩阵,根据引理3.1,这个子矩阵是单调矩阵。又因为\(A[i_1][j]>A[i_2][j]\),显然这个子矩阵的\(pos(j) < i_2\),而根据单调性,\(pos(c)<pos(j)<i_2\),即\(pos(c) \neq i_2\).所以\(A[i_2][c]\)是无用元素。

\(A[S[p]][p]<A[S[p+1]][p]\),根据引理3.2,它在\(p\)之前不是最优决策。又因为在p处也不如\(S[p+1]\)优,显然这个决策没有任何用,可以直接把这个决策删去,然后将后面的决策向左平移一位。
由于p的决策更换了,我们还需要继续比较前一个。对应到伪代码里,就是这一段:

else{
      S.erase(S[p]);//删掉无用决策p
      p--;//将后面的决策向左平移一位,重新比较
 }

否则,我们暂时认为\(S[p]\)是p的决策,然后继续去判定下一列。

if(A[S[p]][p]>A[S[p-1]][p]&&p<N) p++;//继续去判定下一列

\(p=N\),也就是说\(S[N+1]\)在第\(N\)列处都不能变优,那么直接删掉这个决策就可以了。

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,只是保证\(pos(j) \in S\),顺序不一定一样。\(S[p]\)并不是第\(p\)列的真正决策行。我们所能够知道的,仅仅是列\(pos(p) \leq S[p]\)

复杂度证明

该算法的时间复杂度主要消耗在\(p\)的移动和\(S\)删除元素
注意到\(p\)每减少1,必然伴随着\(S\)的删除。也就是说\(p\)的每一次抵消都可以视为花费2的
代价使得\(|S|-1\)
也就是\(|S|\)的减少到\(n\)至多需要花费\(2(M-N)=O(M)\)(我们规定N,M同阶).再加上\(p\)指针移动的\(O(N)\),总的复杂度是\(O(M)\)

真正的SMAWK算法

回到我们之前提到的分治法

对列[l,r]进行分治,维护当前可能成为\(pos\)的行\([x,y]\),令\(mid=\frac{l+r}{2}\),暴力枚举所有可能的行求出\(pos(mid)\),分治递归操作\([l,mid-1] [x,pos(mid)]\)以及\([mid +1,r] [pos(mid),y]\)直至\(l = r\)\(x = y\)。容易发现这样的时间复杂度是\(O(m\log m)\)

我们只是把 "暴力枚举所有可能的行求出\(pos(mid)\)" 这一段优化到了\(O(n)\),总时间复杂度还是\(O(n \log n)\) 。一种更好的解法是每次将所有奇数位的行取出来进行递归,计算出所有奇数位的决策后,再利用决策单调性的性质求出偶数位的决策。

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);
}

\(T(a,b)\)为计算一个\(a \times b(a<b)\)的矩阵的所有pos的时间复杂度,有
\(T(a,b)<T(a/2,a)+O(a+b)\)
由于reduce()之后行数和列数都变成相同,总时间复杂度为\(\Theta(n)+\Theta(\frac{n}{2})+\Theta(\frac{n}{4}) \dots +\Theta(1)=\Theta(n)\)

[NAIPC2016]Jewel Thief(法2)

我们将实现留给读者

[UOJ 285]数据分块鸡(法2)

由于SMAWK的限制较多,此题还需要一些转化才能使用SMAWK算法。
鉴于SMAWK在此题的实际运行时间相比单调队列并不优秀,且可扩展性较差,我们在这里不详细展开,而是讲解一些更实用的决策单调性技巧


番外:

另类决策单调性:区间dp的四边形不等式优化

在区间dp中,我们常常遇到下面这样的状态转移方程:\(f[i][j]=\min_{i \leq k < j} f[i][k]+f[k+1][j]+w(i,j)\)

在这一节中,我们将把一维的决策单调性推广到二维

定理5.1 在状态转移方程\(f[i][j]=\min_{i \leq k < j} f[i][k]+f[k+1][j]+w(i,j)\)中(特别地,\(f[i][i]=w(i,i)=0\)),如果下面两个条件成立:

  1. \(w\)满足四边形不等式
  2. \(\forall a \leq b \leq c \leq d\),有$ w(a,d) \geq w(b,c) \(,该关系也被称为区间包含关系单调。 那么\)f$也满足四边形不等式。

证明:
\(i+1=j\)时,我们有:

\[f[i][j+1]+f[i+1][j]=f[i][i+2]+f[i+1][i+1]=f[i][i+2] \]

\(f[i,i+2]\)的最优决策是\(i+1\),那么有

\[\begin{aligned} f[i][i+2]&=f[i][i+1]+f[i+2][i+2]+w(i,i+2) \\ &=w(i,i+1)+w(i,i+2) \\ &\geq w(i,i+1)+w(i+1,i+2) =f[i][i+1]+f[i+1][i+2] \end{aligned} \]

因此\(f[i][i+2]+f[i+1][i+1] \geq f[i][i+1]+f[i+1][i+2]\)
\(f[i][j+1]+f[i+1][j] \geq f[i][i+1]+f[i+1][i+2]\),\(f\)满足四边形不等式
当决策为\(i\)时同理。

接下来用数学归纳法,假设\(j-i<k\)时,\(f\)满足四边形不等式。考虑\(j-i=k\)的情况,设\(f[i][j+1]\)的最优决策为\(x\),\(f[i+1][j]\)的最优决策是\(y\),不妨设\(i+1 \leq x \leq y\)

对于\([i,j+1]和[i+1,j]\)根据\(x,y\)的最优性有:

\[f[i,j+1]+f[i+1][j]=f[i][x]+f[x+1][j+1]+f[i+1][y]+f[y+1][j]+w(i,j+1)+w(i+1,j) \ \ (5.1) \]

对于\([i,j]和[i+1,j+1]\),\(x\)\(y\)不一定最优

\[f[i][j]+f[i+1][j+1] \leq f[i][x]+f[x+1][j]+f[i+1][y]+f[y+1][j+1]+w(i,j)+w(i+1,j+1) \ \ (5.2) \]

根据归纳假设,有

\[f[x+1][j+1]+f[y+1][j] \geq f[x+1][j]+f[y+1][j+1] \tag{5.4} \]

因为\(w\)满足四边形不等式,有

\[w(i,j+1)+w(i+1) \geq w(i,j)+w(i+1)(j+1) \tag{5.5} \]

\((5.4)+(5.5)\),得

\[f[i][x]+f[x+1][j+1]+f[i+1][y]+f[y+1][j]+w(i,j+1)+w(i+1,j) \geq f[i][x]+f[x+1][j]+f[i+1][y]+f[y+1][j+1]+w(i,j)+w(i+1,j+1) \]

代入\((5.1)(5.2)\),有

\[f[i,j+1]+f[i+1][j] \geq f[i][j]+f[i+1][j+1] \]

四边形不等式成立。证毕。

定理5.2 在状态转移方程\(f[i][j]=\min_{i \leq k < j} f[i][k]+f[k+1][j]+w(i,j)\)中(特别地,\(f[i][i]=w(i,i)=0\)),设\(p[i][j]\)\(f[i][j]\)取到最小值时的\(k\)
如果\(f\)满足四边形不等式,那么\(\forall i < j,p[i,j-1] \leq p[i,j] \leq p[i+1,j]\)

证明:

\(p=p[i][j]\),对于\(\forall i < k \leq p\),因为\(f\)满足四边形不等式,有:

\[f[i][p]+f[i+1][k] \geq f[i][k]+f[i+1][p] \tag{5.5} \]

根据\(p\)的最优性,有:

\[f[i][k]+f[k+1][j] \geq f[i][p]+f[p+1][j] \tag{5.6} \]

\((5.5)+(5.6)\),得:

\[f[i+1][k]+f[k+1][j] \geq f[i+1][p]+f[p+1][j] \]

\[\therefore f[i+1][k]+f[k+1][j]+w(i+1,j)\geq f[i+1][p]+f[p+1][j]+w(i+1,j) \]

容易看出这是\(f[i+1][j]\)的转移,也就是说,对于\([i+1,j]\),选\(p\)转移比选小于\(p\)的任意决策\(k\)更优。所以\(f[i+1][j]\)的最优决策一定在\(p\)及以后取到。所以\(p[i+1][j] \geq p[i][j]\)
同理可证\(p[i][j-1] \leq p[i][j]\)

那么,当\(w\)满足四边形不等式和区间包含关系单调,我们就可以利用上述两个定理优化DP.这种优化不需要任何数据结构。转移时\(i,j\)照常枚举,而\(k\)只需要从\(p[i,j-1]\)枚举到\(p[i+1,j]\)
总复杂度为:

\[\sum_{i=1}^{n-1} \sum_{j=i+1}^n (p[i+1][j]-p[i][j-1]+1) \]

类似尺取法的复杂度分析,总的复杂度不是\(O(n^3)\)而是\(O(n^2)\)

[NOI1995]石子合并(加强版)

\(n\)堆石子排成一个环,每堆石子有\(a_i\)个。一开始每个石子为单独一堆。可以把相邻的两堆石子合并为一堆,合并的代价为两堆石子的个数之和,求把所有石子合并成一堆的最小代价和最大代价。\(n \leq 2500\)

首先把环复制一遍,断环为链。
\(w(i,j)\)\([i,j]\)的区间和。容易写出两个dp方程:
\(f[i][j]=\min(f[i][k]+f[k+1][j]+w(i,j)\)
\(g[i][j]=\max(g[i][k]+g[k+1][j]+w(i,j)\)

显然\(w\)满足四边形不等式,因此\(f\)有决策单调性,可以用上面提到的方法\(O(n^2)\)求出。

贪心考虑,\(g[i][j]\)的最优转移一定是\(g[i][j-1]\)\(g[i+1][j]\),因为这样合并最不“均衡”,可以让大的被合并很多次。严格证明可以用反证法。

另外,此题有\(O(n \log n)\)的非动态规划算法,这超出了我们讨论的范围。

#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

在这一节中,我们抛开四边形不等式,用决策单调性最朴素的定义来解决问题题。

给出一个长度为\(n\)的序列\(a_i\),把这个序列划分为几块,满足从前往后块内的权值和严格递增。求每个块权值的平方之和的最小值
\(n \leq 4 \times 10^7,a_i \leq 10^9\)

\(a\)的前缀和为\(s\).设\(f[i]\)表示前\(i\)个数,且最后一块的右端点为\(i\)的最小值。\(g[i]\)表示取到最小值时最后一块的左端点(不包含),实际上就是\(f[i]\)的最优转移。

那么有:

\[f[i]=\min_{j=0}^{i-1}(f[j]+sum[i]-sum[j]) \ \ (s[i]-s[j]>s[j]-s[g[j]]) \]

直接转移是\(O(n^2)\)的,且没有什么优化空间. 容易发现,如果知道了所有的\(g\),那么\(f\)的值可以根据\(g\)推出,直接沿着\(g\)数组往前跳即可。下面的代码就求出了\(f[n]\).

int x=n;
while(x>0){
	ans+=(s[x]-s[g[x]])*(s[x]-s[g[x]]);
	x=g[x];
}

那么,\(g\)是否有特殊的性质呢?

引理6.1: 在最优的划分方案中,最后一块的和应该尽量小。形式化的说,$$g[i]=\max_{j=0}^{i-1}(j) (s[i]-s[j]>s[j]-s[g[j]]) \tag{6.1}$$ (\(j\)越大,根据前缀和的单调性知区间和越小)

感性理解,\(a^2+b^2<(a+b)^2\),所以把大段拆分成小段更优。严谨证明如下:

\(i \leq 2\),满足条件的\(j\)至多有一个,结论显然成立

假设$i < x $ 时结论成立,现在我们要证明\(i=x\)时结论也成立,用反证法。

\(i=x\)时结论不成立,令\(y=g[x]\)那么一定存在\(k< y,s[k]-s[g[k]]<s[x]-s[k]\) 使得

\[f[k]+(s[x]-s[k])^2<f[y]+(s[x]-s[y])^2 \]

展开并移项,有

\[f[y]-f[k]>s[k]^2-s[y]^2+2s[x]s[y]-2s[x]s[k] \]

对右边因式分解,有

\[f[y]-f[k]>(s[y]-s[k])(2s[x]-s[y]-s[k]) \tag{6.2} \]

\(\because x>y\),所以\(s[x]>s[y]\)

\(\therefore s[x]-s[k]>s[y]-s[k]\)

左边加上\(s[x]-s[y]>0\),有\(2s[x]-s[y]-s[k]>s[y]-s[k]\)

代入\((6.2)\),有\(f[y]-f[k]>(s[y]-s[k])^2\),即\(f[y]>f[k]+(s[y]-s[k])^2\). 也就是说,把\((k,y]\)分为一段会得到一个更小的\(f[y]\),这与\(f\)的最小性矛盾。因此原命题成立。

根据数学归纳法,原命题恒成立。

根据引理,我们发现这题的决策单调性不仅是简单的\(g_i\)单调递增,并且\(g_i\)还满足一些特殊的性质。那么就可以根据这些性质快速求出决策\(g[i]=\max_{j=0}^{i-1}(j) (s[i]-s[j]>s[j]-s[g[j]])\)

把条件转换一下,变成$ s[i]>2s[j]-s[g[j]] \(.由于\)s[i]$也单调递增,维护一个按照\(2s[j]-s[g[j]]\)递增的单调队列即可。

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);
}

总结

我们在本文中一共提到了决策单调性的四种解法:

  1. 单调队列+二分查找
  2. 斜率优化
  3. 分治
  4. SMAWK算法

文中提到的算法各有优劣

线性的斜率优化算法在所有算法中是时间复杂度最优秀的算法,但在使用时的局限性也最大。当函数的变化比较平缓时,决策单调性中不同的决策点比较少,此时分治法与二分法的询问次
数较少,有时在随机数据下能够做到期望线性。而当函数比较陡峭(如NOI2009《诗人小G》
)时,队列中元素较多,二分算法的询问次数就是接近\(O(N \log N)\)的。而线性算法的询问次
数则比较稳定,这也启示我们应该根据题目选择更为合适的算法来解题。

实际上,一般的题目用单调队列+二分查找和分治法就足够了,即使时间复杂度上稍有不足,但适当的常数优化可以提高程序的执行效率。因此,我们建议读者熟练掌握以上两种方法。


参考资料:

冯哲《浅谈决策单调性动态规划的线性解法》

毛子青《动态规划算法的优化技巧》

李煜东《算法竞赛进阶指南》

UOJ 数据分块鸡 官方题解

posted @ 2020-05-22 16:28  birchtree  阅读(2331)  评论(2编辑  收藏  举报