决策单调性优化dp

好久没做过了
其有两种形式:
1.1d/1d dp(f_i=min(f_j+w(j+1,i)))
它的特点是对于决策点(a,b)(a<b),如果(a)转移到(c)(b)转移到(c)差,则以后(a)转移到(d)永远比(b)转移到(d)
这说明决策点是单调递增的。
(a<b<c<d,f_b+w(b+1,c)leq f_a+w(a+1,c))
如果(w(b+1,d)+w(a+1,c)leq w(a+1,d)+w(b+1,c))
两者相加
(f_b+w(b+1,d)leq f_a+w(a+1,d))
证明成功。
引理:如果有(w(a,b+1)+w(a+1,b)geq w(a+1,b+1)+w(a,b))成立,则四边形不等式成立。
实际上,决策点也可以单调递减。
它有两种求法:
分治法:类似整体二分。
每次考虑更新中点。
如果要从数组(c)转移到(d),代码如下:

void fz(int *c,int *d,int l,int r,int a,int b){
	if(l>r)
		return;
	int md=(l+r)/2,ans=l;
	for(int i=l;i<=min(md,b);i++){
		if(d[md]>c[i-1]+va(i,md)){
			d[md]=c[i-1]+va(i,md);
			ans=i;
		}
	fz(c,d,l,md-1,a,ans);
	fz(c,d,md+1,r,ans,b);
}

(ans)事实上代表([l,md])最大转移点
根据决策单调性的定义,右边([md+1,r])区间的转移点肯定大于等于(ans),所以决策点被锁定到了([l,md],[md+1,r])两个区间内。
ans的初值依题目而定。
分治法的优点:
1.好写好理解,不容易出错
2.如果(va)函数不容易求出,但是它移动一格(从([l,r])移动到([l,r-1],[l,r+1],[l+1,r],[l-1,r]))容易求出(比如区间逆序对函数,直接做只能(O(sqrt{n})),但是移动一格可以用bit维护)
那么(va)函数总移动次数是(O(nlog_2n))的。
怎么移动?用类似莫队移动的方式
缺点:只能从(c)转移到(d),如果做1d/1d转移需要cdq分治,复杂度更高
二分+单调队列/栈法
以队列为例。假设决策点是单调递增的。
一个点(i)(j)(i<j))好的决策时间是区间([1,k]),可以二分。
维护可能成为决策点的单调队列,队列中相邻元素(a_i<a_{i+1})且单调队列中(a_i)取代(a_{i+1})的时间不下降。
在插入新的决策点时,插入到队列的尾部。
此时队尾可能有一些点不满足单调性,要删除。
所以要维护双端队列。
然后考虑队头的答案。
原来队头的答案事实上可能是不优的。
如果队列中第二个元素取代队头元素的时间小于当前决策点(x),则队头元素显然是不优的,可以删除。
重复以上过程,队头元素就是答案。
单调栈也类似。
维护可能成为决策点的单调栈,栈中元素编号从顶到底单调递减,且栈中(a_i)取代(a_{i+1})的时间不下降。
在插入新的决策点时,插入到栈的顶部。
此时栈顶可能有一些点不满足单调性,要删除。
然后考虑栈顶的答案。
原来栈顶的答案事实上可能是不优的。
如果栈顶中第二个元素取代栈顶元素的时间小于当前决策点(x),则栈顶元素显然是不优的,可以删除。
重复以上过程,栈顶元素就是答案。
栈+二分代码如下:

int cl(int a,int b){
	return f[a-1]+s[a]*b*b;
}
int ef(int x,int y){
	int l=1,r=n,ans=n+1;
	while(l<=r){
		int md=(l+r)/2;
		if(cl(x,md-t[x]+1)>=cl(y,md-t[y]+1)){
			ans=md;
			r=md-1;
		}
		else
			l=md+1;
	}
	return ans;
}
for(int i=1;i<=n;i++){
	while(st.size()>1&&ef(st[st.size()-2],st[st.size()-1])<=ef(st[st.size()-1],i))
		st.pop_back();
	st.push_back(i);
	while(st.size()>1&&ef(st[st.size()-2],st[st.size()-1])<=i)
		st.pop_back();
	int x=st.back();
	f[i]=cl(x,i-x+1);
}

(cl(x,len))表示决策点为(x),区间长度为(i-x+1)的代价。
这份代码求的是dp最大值。

SMAWK算法:可以线性决策单调性,但是没什么用
如果(f_i=min(f_j+w(j+1,i)))但是(w(b+1,d)+w(a+1,c)geq w(a+1,d)+w(b+1,c)),决策是单调递减的
这是因为(a<b<c<d,f_b+w(b+1,c)geq f_a+w(a+1,c))
两者相加,(f_b+w(b+1,c)+w(b+1,d)+w(a+1,c)geq f_a+w(a+1,c)+w(a+1,d)+w(b+1,c))
(f_b+w(b+1,d)geq f_a+w(a+1,d))
证明成功。

决策点是否递增是判定使用单调栈/队列的因素。

2.2d/1d dp:形如(f_{i,j}=min(f_{i,k}+f_{k+1,j}+w(i,j)))
如果dp的代价函数满足四边形不等式。
(s_{i,j-1}leq s_{i,j}leq s_{i+1,j})
转移分割点就从(s_{i,j-1})转移到(s_{i+1,j})
这样子均摊时间复杂度是(O(n^2))的。
这是因为每次转移的(s)都在一条斜线上。
转移复杂度是((s_{i,n}-s_{i-1,n-1})+(s_{i-1,n-1}-s_{i-2,n-2})+(s_{i-2,n-2}-s_{i-3,n-3})=s_{i,n})
这最多是(O(n))的。
所以均摊时间复杂度是(O(n^2))的。

事实上,如果dp满足决策单调性,那么它关于选择的段数是凸的。
Itst的博客中有证明。

考场上看出规律经常只能打表。

例题:
1.lg5574
容易列出方程(f_{k,i}=min(f_{k-1,j})+w(j+1,i))
可以证明(w)满足四边形不等式
但是事实上发现(w)很难快速求出,所以不能二分+队列。
但是它移动一格相当于查询一个区间内(<v,>v)的值的个数
用bit维护

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 25010
int n,k,bt[N],a[N],L,R,ans,f[30][N];
void ad(int x,int y){
	for(;x<=n;x+=x&-x)
		bt[x]+=y;
}
int qu(int x){
	int ans=0;
	for(;x;x-=x&-x)
		ans+=bt[x];
	return ans;
}
int va(int l,int r){
	while(l<L){
		L--;
		ans+=qu(a[L]);
		ad(a[L],1);
	}
	while(r>R){
		R++;
		ans+=R-L-qu(a[R]);
		ad(a[R],1);
	}
	while(l>L){
		ad(a[L],-1);
		ans-=qu(a[L]);
		L++;
	}
	while(r<R){
		ad(a[R],-1);
		ans-=R-L-qu(a[R]);
		R--;
	}
	return ans;
}
void fz(int *c,int *d,int l,int r,int a,int b){
	int md=(l+r)/2,po=a;
	for(int i=a;i<=min(md-1,b);i++){
		int vt=(md-i+1)*(md-i)/2-va(i,md);
		if(d[md]>c[i-1]+vt){
			d[md]=c[i-1]+vt;
			po=i;
		}
	}
	if(l==r){
		return;
	}
	fz(c,d,l,md,a,po);
	fz(c,d,md+1,r,po,b);
}
signed main(){
	scanf("%lld%lld",&n,&k);
	for(int i=1;i<=n;i++)
		scanf("%lld",&a[i]);
	for(int i=1;i<=n;i++){
		f[1][i]=f[1][i-1]+qu(a[i]);
		ad(a[i],1);
	}
	ans=n*(n-1)/2-f[1][n];
	L=1;
	R=n;
	for(int i=2;i<=k;i++){
		memset(f[i],127,sizeof(f[i]));
		fz(f[i-1],f[i],1,n,1,n);
	}
	printf("%lld
",f[k][n]);
}

1.1 CF833B
又是模板题,没什么营养价值。
区间颜色个数用可持久化线段树/桶维护即可。

#include<bits/stdc++.h>
using namespace std;
#define N 35010
int n,k,a[N],L,R,ans,f[60][N],bc[N];
int va(int l,int r){
	while(l<L){
		L--;
		if(!bc[a[L]])
			ans++;
		bc[a[L]]++;
	}
	while(r>R){
		R++;
		if(!bc[a[R]])
			ans++;
		bc[a[R]]++;
	}
	while(l>L){
		bc[a[L]]--;
		if(!bc[a[L]])
			ans--;
		L++;
	}
	while(r<R){
		bc[a[R]]--;
		if(!bc[a[R]])
			ans--;
		R--;
	}
	return ans;
}
void fz(int *c,int *d,int l,int r,int a,int b){
	int md=(l+r)/2,po=a;
	for(int i=a;i<=min(md,b);i++){
		int vt=va(i,md);
		if(d[md]<c[i-1]+vt){
			d[md]=c[i-1]+vt;
			po=i;
		}
	}
	if(l==r)
		return;
	fz(c,d,l,md,a,po);
	fz(c,d,md+1,r,po,b);
}
signed main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(int i=1;i<=n;i++){
		f[1][i]=f[1][i-1];
		if(!bc[a[i]])
			f[1][i]++;
		bc[a[i]]++;
	}
	ans=f[1][n];
	L=1;
	R=n;
	for(int i=2;i<=k;i++)
		fz(f[i-1],f[i],1,n,1,n);
	printf("%d
",f[k][n]);
}

1.2.CF868F
还是容易列出方程(f_{k,i}=min(f_{k-1,j})+w(j+1,i))
可以证明(w)满足四边形不等式。
但是事实上发现(w)很难快速求出,所以不能二分+队列。
移动一格相当于查询一个区间(=v)的值的个数
用桶维护,每次移动时加/减桶对应的那一位即可。

#include<bits/stdc++.h>
using namespace std;
#define N 100010
#define int long long
int n,k,a[N],bc[N],l=1,r=1,va,f[20][N];
void ad(int x){
	va+=bc[x];
	bc[x]++;
}
void dl(int x){
	bc[x]--;
	va-=bc[x];
}
int qu(int x,int y){
	while(l<x){
		dl(a[l]);
		l++;
	}
	while(l>x){
		l--;
		ad(a[l]);
	}
	while(r<y){
		r++;
		ad(a[r]);
	}
	while(r>y){
		dl(a[r]);
		r--;
	}
	return va;
}
void fz(int p,int l,int r,int x,int y){
	if(l>r||x>y)return;
	int o,md=(l+r)/2;
	for(int i=x;i<=min(md,y);i++)
		if(f[p-1][i]+qu(i+1,md)<f[p][md]){
			f[p][md]=f[p-1][i]+qu(i+1,md);
			o=i;
		}
	fz(p,l,md-1,x,o);
	fz(p,md+1,r,o,y);
}
signed main(){
	memset(f,32,sizeof(f));
	f[0][0]=0;
	cin>>n>>k;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	bc[a[1]]++;
	for(int i=1;i<=k;i++)
		fz(i,1,n,0,n-1);
	cout<<f[k][n];
}

2.诗人小G
模板题
3.IOI2014 holiday
容易发现,获取权值和移动的过程是独立的。
如果我们要获取权值(S),则可以考虑(S)的每个点,在遍历到(S)时获取(S)的该节点权值即可。
显然,对于固定的出发点,(S)是个区间。
贪心的想,遍历区间([l,r])最小费用是(min(p-l,r-p)+r-l)
暴力就是枚举区间([l,r]),求出最小费用(d),显然我们要获取的权值就是区间前(k-d)大。
用可持久化线段树求出。
考虑枚举(l),暴力枚举(r)不可取。
对于固定的(l),发现(l)对应的决策点(f(l))是单调递增的((f(l)leq f(l+1))
用经典的分治法求即可。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 120010
int n,p,d,a[N],s[2000010],aa,ct,b[N],t[N];
signed lc[2000010],rc[2000010],sz[2000010],rt[N];
void ins(signed &o,int p,int l,int r,int x,int v){
	o=++ct;
	sz[o]=sz[p]+1;
	s[o]=s[p]+v;
	lc[o]=lc[p];
	rc[o]=rc[p];
	if(l==r){
		return;
	}
	int md=(l+r)/2;
	if(x<=md)
		ins(lc[o],lc[p],l,md,x,v);
	else
		ins(rc[o],rc[p],md+1,r,x,v);
}
int qu(int o,int p,int l,int r,int x){
	if(l==r)
		return min(x,1ll*(sz[o]-sz[p]))*t[l];
	int md=(l+r)/2;
	if(x>sz[rc[o]]-sz[rc[p]])
		return qu(lc[o],lc[p],l,md,x-sz[rc[o]]+sz[rc[p]])+s[rc[o]]-s[rc[p]];
	return qu(rc[o],rc[p],md+1,r,x);
}
void fz(int l,int r,int a,int b){
	int ans,va=-1,md=(l+r)/2;
	for(int i=a;i<=b;i++){
		int t=d-(i-md)-min(i-p,p-md);
		if(t>0)
			t=qu(rt[i],rt[md-1],1,n,t);
		else
			t=0;
		if(t>va){
			ans=i;
			va=t;
			aa=max(aa,t);
		}
	}
	if(l==r)
		return;
	fz(l,md,a,ans);
	fz(md+1,r,ans,b);
}
signed main(){
	scanf("%lld%lld%lld",&n,&p,&d);
	p++;
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		t[i]=a[i];
	}
	sort(t+1,t+n+1);
	for(int i=1;i<=n;i++)
		b[i]=lower_bound(t+1,t+n+1,a[i])-t;
	for(int i=1;i<=n;i++)
		ins(rt[i],rt[i-1],1,n,b[i],a[i]);
	fz(1,p,p,n);
	printf("%lld",aa);
}

3.1 JOISC2019 蛋糕拼接
如果我们钦定了选择蛋糕的集合,考虑如何求出最大权值。
它的(k)值之和显然是不变的,我们要最小化(C)值。
考虑贪心的经典方法:排序,把(C)排序。
显然,选择排序后连续递增的(C)是最优的,费用为(2(C_{max}-C_{min}))
(C)从小到大排序,则费用为(sum V-2(C_{r}-C_{l}))
如果确定了(l,r),显然我们会选择最大的(m-2)(k),用可持久化线段树求出。
还是考虑枚举(l),暴力枚举(r)不可取。
对于固定的(l),发现(l)对应的决策点(f(l))是单调递增的((f(l)leq f(l+1))
用经典的分治法求即可。
注意细节,最优决策点一开始要赋值成区间的尾部

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 200010
int n,m,s[N*31],aa=-1e18,ct,lc[N*31],rc[N*31],sz[N*31],rt[N];
struct no{
	int v,c;
}a[N];
int operator <(no x,no y){
	return x.c<y.c;
}
void ins(int &o,int p,int l,int r,int x){
	o=++ct;
	sz[o]=sz[p]+1;
	s[o]=s[p]+x;
	lc[o]=lc[p];
	rc[o]=rc[p];
	if(l==r){
		return;
	}
	int md=(l+r)/2;
	if(x<=md)
		ins(lc[o],lc[p],l,md,x);
	else
		ins(rc[o],rc[p],md+1,r,x);
}
int qu(int o,int p,int l,int r,int x){
	if(l==r)
		return min(x,sz[o]-sz[p])*l;
	int md=(l+r)/2;
	if(x>sz[rc[o]]-sz[rc[p]])
		return qu(lc[o],lc[p],l,md,x-sz[rc[o]]+sz[rc[p]])+s[rc[o]]-s[rc[p]];
	return qu(rc[o],rc[p],md+1,r,x);
}
void fz(int l,int r,int L,int R){
	int ans=R,va=-1e18,md=(l+r)/2;
	for(int i=max(L,md);i<=R;i++)
		if(i-md+1>=m){
			int v=-(a[i].c-a[md].c)*2+a[i].v+qu(rt[i-1],rt[md],0,1e9,m-2)+a[md].v;
			if(v>va){
				ans=i;
				va=v;
				aa=max(aa,v);
			}
		}
	if(l==r)
		return;
	fz(l,md,L,ans);
	fz(md+1,r,ans,R);
}
signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%lld%lld",&a[i].v,&a[i].c);
	}
	sort(a+1,a+n+1);
	for(int i=1;i<=n;i++)
		ins(rt[i],rt[i-1],0,1e9,a[i].v);
	fz(1,n,1,n);
	printf("%lld",aa);
}

4.Mowing Mischief
(f_i)表示以(i)结尾的最小权值,则(f_i=min(f_j+(x_i-x_j)*(y_i-y_j)),g_j+1=g_i,x_i<x_j,y_i<y_j)
(g)(lis)数组,可以用bit求出。
求出(g)后,如果按照(g_j+1=g_i)的所有(j)按照(x)排序,则(y)是递减的。
发现这个dp可以决策单调性。
我们要解决(x_i<x_j,y_i<y_j)的问题
如果我们转移到节点(i)时,则合法的横坐标是一段区间,且左/右端点递增。
用线段树,把当前点挂在线段树的(log_2n)个区间后,对每个区间运行决策单调性后更新答案即可。
我们并没有利用到左/右端点递增这个性质,利用这个性质可以得到更为优秀的做法。
考虑分块,如果用二分+队列法优化dp,则能够转移到当前点的是一个前缀,需要满足每个区间和块的交是块的前缀/后缀。
而且每个区间最多包含常数个块。
方法:用贪心,要让当前区间尽量大,以跨过尽量少的块。
对于当前要寻找的区间,找到最右的右端点,使得这个区间不严格包含任意一个区间。
这样子即可满足条件。
此法也可用于凸包优化dp。
5.记忆的轮廓

#include<bits/stdc++.h>
using namespace std;
#define N 1510
int v[N*2],nxt[N*2],h[N],ec,n,m,p,d[N],vi[N],T;
#define db double
db a[N][N],g[N],s[N],f[N][N];
void add(int x,int y){
	d[x]++;
	v[++ec]=y;
	nxt[ec]=h[x];
	h[x]=ec;
}
void dfs(int x){
	vi[x]=1;
	g[x]=1;
	for(int i=h[x];i;i=nxt[i]){
		dfs(v[i]);
		g[x]+=(1.0/d[x])*g[v[i]];
	}
}
int main(){
	scanf("%d",&T);
	while(T--){
		memset(h,0,sizeof(h));
		ec=0;
		memset(a,0,sizeof(a));
		memset(d,0,sizeof(d));
		memset(g,0,sizeof(g));
		memset(s,0,sizeof(s));
		memset(vi,0,sizeof(vi));
		scanf("%d%d%d",&n,&m,&p);
		for(int i=0;i<=n+2;i++)
			for(int j=0;j<=n+2;j++)
				f[i][j]=1e18;
		for(int i=1;i<=m-n;i++){
			int x,y;
			scanf("%d%d",&x,&y);
			add(x,y);
		}
		for(int i=1;i<n;i++)
			d[i]++;
		for(int i=1;i<=n;i++)
			if(!vi[i])
				dfs(i);
		for(int i=1;i<=n;i++)
			for(int j=h[i];j;j=nxt[j])
				s[i]+=g[v[j]];
		for(int i=1;i<=n;i++)
			for(int j=i+1;j<=n;j++)
				a[i][j]=a[i][j-1]*d[j-1]+d[j-1]+s[j-1];
		f[1][1]=0;
		for(int i=2;i<=p;i++){
			for(int j=1;j<=n;j++)
				for(int k=j-1;k;k--){
					f[i][j]=min(f[i][j],f[i-1][k]+a[k][j]);
					if(k-j>=40)
						break;
				}
		}
		printf("%.4lf
",f[p][n]);
	}
}

6.交与并
引理1:如果A包含B,则删除B对答案不会有影响
引理2:选的区间最多有2个。
考虑把剩下的区间按照左端点从小到大排序,则两个区间(i,j,i<j)的代价为((r_j-l_i)*(r_i-l_j))
不难发现对于(i),对应的(j)最优决策点递增,用决策单调性优化即可。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 1000010
struct no{
	int l,r;
}a[N],st[N];
int operator <(no x,no y){
	return x.l<y.l||(x.l==y.l&&x.r>y.r);
}
int n,tp,ans;
void fz(int l,int r,int L,int R){
	int md=(l+r)/2;
	int po=max(L,md+1),va=-1e18;
	for(int i=max(L,md+1);i<=R;i++)
		if((st[i].r-st[md].l)*(st[md].r-st[i].l)>va){
			va=(st[i].r-st[md].l)*(st[md].r-st[i].l);
			ans=max(ans,va);
			po=i;
		}
	if(l>=r)
		return;
	fz(l,md-1,L,po);
	fz(md+1,r,po,R);
}
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++){
		scanf("%lld%lld",&a[i].l,&a[i].r);
	}
	sort(a+1,a+n+1);
	int p=0;
	for(int i=1;i<=n;i++){
		if(p>=a[i].r)
			ans=max(ans,(st[tp].r-st[tp].l)*(a[i].r-a[i].l));
		else{
			st[++tp]=a[i];
			p=a[i].r;
		}
	}
	fz(1,tp,1,tp);
	printf("%lld
",ans);
}

7.柠檬
引理:每一段的首/尾一定是相同的,且当前段选择的一定是首/尾。
证明:把首/尾单成一段,答案肯定是更大的。
(f_i)表示最后一段是(i)的最大价值。
枚举(j)使得([j...i])是一段,(f_i=max(f_{j-1}+a_i(c_i-c_j+1)^2))
后面的用决策单调性二分栈维护。
8.jzoj5427
9.monster
伤害的最小值=和-抵挡伤害的最大值
引理:如果叠甲,则当护甲失效前不会再次叠甲
证明:否则把当前叠的甲移动到上次叠甲的位置答案不会更差。
考虑以未叠甲的1s和"叠了甲影响的区域"划分状态后dp,设(f_{i,j})表示时间为(i),共叠了(j)甲的最小代价
(c_{i,j})表示第(i)秒叠(j)甲后能够抵挡的伤害,显然有转移方程:(c_{i,j}=c_{i+1,j-1}+max(a_i-j,0))
(f)的转移:
1.不叠甲,(f_{i,j}=min(f_{i,j},f_{i-1,j}))
2.枚举哪里叠甲,(f_{i,j}=min(f_{k,j-i+k}+c_{k,i-k},f_{i,j}))
时间复杂度(O(n^3))
1转移不是瓶颈,考虑优化2的转移。
2转移事实上(j-i)是恒定的,在一条斜线上。(f_{i,j}=min(f_{i-k,j-k}+c_{i-k,k},f_{i,j}))
显然(ileq j)
枚举(j-i=l),设(g_i=f_{i,i+l})(w(a,b)=c_{a,b-a})(g_i=min(g_{i-k}+w(i-k,i)))
这个dp可以决策单调性
证明:事实上,我们要寻找(c_{a,c-a}+c_{b,d-b})(c_{b,c-b}+c_{a,d-a})的关系
考虑转化模型:叠一次甲等于画一个三角形,(a_i)相当于一个位置为(i),高是(a_i)的柱子,要让被三角形覆盖的柱子面积最多
发现,(c_{a,c-a}-c_{b,c-b})(c_{b,d-b}-c_{a,d-a})事实上都是一个高是(b-a)的矩形,但是(c_{b,d-b}-c_{a,d-a})的底边/顶边更高。
所以(c_{a,c-a}+c_{b,d-b}<c_{b,c-b}+c_{a,d-a}),所以(w(a,c)+w(b,d)<w(b,c)+w(a,d))
于是我们可以用决策单调性优化dp
10.Post加强版
又是裸题
11.环状邮局
做法1:钦定起始点,设当前起始点为0,其他点旋转一下。
然后做暴力dp
做法2:暴力dp用带权二分+二分队列优化
做法3:先求出一定选择(0)点的解。
由于决策单调性,所以其他钦定起点的路径与其是交错的(假设钦定(0)点的路径是(0,a_1...a_{k-1}),任意钦定起点的路径是(x,b_1...b_{k-1}),则不可能存在(b_{i-1},b_i)使得存在一个(y)(a_{y-1}leq b_{i-1},b_ileq a_{y})
考虑找到(a_{y}-a_{y-1})最小的一段,这一段显然有一个最优解的决策点
然后在这一段中钦定起点,运行(O(nklog_2n))的二分+队列dp
由抽屉原理,(a_{y}-a_{y-1})最小的一段的点数不超过(frac{n}{k}),时间复杂度(O(n^2log_2n))
做法4:求出一定选择(0)点的解后,找到(a_{y}-a_{y-1})最小的一段,然后把这一段的起点设成(0)点,其他的点同时旋转。
建立平面直角坐标系:横坐标表示在原环上的坐标,纵坐标表示当前在第几个。
对于一个钦定选择(x)的解,画出一条折线,设路径是(x,b_1...b_{k-1}),画((x,0),(b_1,1)......(b_{k-1},k-1))
路径不交错就代表折线不交。
我们现在求出了(0)开头的解,由路径交错,我们就知道了每个解所在的区间。
求出一个折线,可以用分治:一个点一个点的求出最优解。
这样子的时间复杂度是(((a_1)+(a_2-a_1+1)+....)log_2n),是(O(nlog_2n))的。
考虑在外层分治,找到起始点为中点的折线。
然后我们就可以确定([l,md-1],[md+1,r])的折线的每个决策点的取值范围了。
时间复杂度(O(nlog_2n(log_2n+log_2V)))
12.uoj187
考虑dp,考虑以"完成接水果连续段"划分状态,每次添加未接水果连续段,接水果连续段补全序列。
(f_i)表示(i)一定要接的最大价值,(g_i)表示(i)能够接到的
转移:(f_i=max(f_j+(j-i+1)^2+g_j)),满足接(j)后能接(i)(j)是上一个连续段的末尾。
考虑优化:把每个点的(坐标,时间)放在平面上,一个点(i)能够转移到另一个点(j)要满足(i)(j)引出的斜率为1,-1的直线构成的半平面上。
把坐标系逆时针旋转45度,则条件变成二维偏序。
把所有点按照(x)坐标排序,在转移时还要满足(j)的时间小于(i)的时间,但是(j)能够转移到(i)蕴含了这个条件,所以没有问题
(g)转移是简单的二维偏序问题,用bit维护前缀最大值即可。
(f)可以考虑把点数列划分成极大段,使得段内的x,y都递减。
如果(i,j)在同一段中,则(j)能够转移到(i)
转移把括号拆开后是斜率优化形式,可以用半平面交解决。
要维护多个栈,用vector维护
当然也可以决策单调性,函数满足决策单调性,可以用决策单调性转移。
代码里用的斜率优化。

#include<bits/stdc++.h>
using namespace std;
#define N 500010
#define int long long
struct no{
	int x,y,iy,id;
}a[N];
struct nn{
	int k,b;
	double x;
	int f(int x){
		return k*x+b;
	}
};
int f[N],id[N],ct,bt[N],b[N],cc,le[N],g[N],ans;
void mod(int x,int y){
	for(;x<=cc;x+=x&-x)
		bt[x]=max(bt[x],y);
}
int qu(int x){
	int ans=0;
	for(;x;x-=x&-x)
		ans=max(ans,bt[x]);
	return ans;
}
double jd(nn x,nn y){
	return 1.0*(x.b-y.b)/(y.k-x.k);
}
struct sg{
	vector<nn>v;
	void ins(nn x){
		while(v.size()>1&&jd(x,v.back())>=v.back().x)
			v.pop_back();
		v.push_back(x);
		if(v.size()>1){
			v.back().x=jd(x,v[v.size()-2]);
		}
	}
	int q(int x){
		if(!v.size())
			return -1e18;
		while(v.size()>1&&v.back().x<=x)
			v.pop_back();
		return v.back().f(x);
	}
}sg[N];
int n;
int operator <(no x,no y){
	return x.x<y.x||(x.x==y.x&&x.y<y.y);
}
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++){
		int c,d;
		scanf("%lld%lld",&c,&d);
		a[i]=(no){d-c,d+c,0,i};
		b[++cc]=d+c;
	}
	id[1]=1;
	ct=1;
	for(int i=2;i<=n;i++){
		if(a[i-1].x<=a[i].x&&a[i-1].y<=a[i].y)
			id[i]=id[i-1];
		else{
			id[i]=++ct;
		}
	}
	sort(a+1,a+n+1);
	sort(b+1,b+cc+1);
	cc=unique(b+1,b+cc+1)-b-1;
	for(int i=1;i<=n;i++)
		a[i].iy=lower_bound(b+1,b+cc+1,a[i].y)-b;
	for(int i=1;i<=n;i++){
		g[i]=qu(a[i].iy);
		int x=a[i].id;
		sg[id[x]].ins((nn){-2*x,g[i]+x*x-2*x});
		f[i]=max(f[i],sg[id[x]].q(x)+x*x+2*x+1);
		mod(a[i].iy,f[i]);
		ans=max(ans,f[i]);
	}
	printf("%lld",ans);
}

13.shoes
计算代价可以把每双鞋按照平均值排序,则每双鞋装的柜子都是连续的。
可以发现一对鞋子的代价是分段的凸函数:在((0,a_i))处是斜率为(-1)的直线,在([a_i,b_i])处是斜率为(-2)的直线,在((b_i,inf))处是斜率为(2)的直线。
考虑以放置一对鞋子为过程划分状态。
每有一双鞋,设(f(x))是鞋子放(x)位置的最优值,(x)就会加上下凸分段函数。
由中位数定律,把([l,r])的所有鞋子从小到大排序,中间的鞋子的位置就是([l,r])的鞋子的最优放置位置。
中位数可以用可持久化线段树找。
考虑每只鞋子的贡献:如果放置位置在左边,则ans+=它的位置-鞋柜的位置,否则and+=鞋柜的位置-它的位置。
这也可以用可持久化线段树维护。
发现这个函数满足四边形不等式,于是可以决策单调性。
用经典的分治/二分队列法即可。

#include<bits/stdc++.h>
#define N 5000010
using namespace std;
#define int long long
int lc[N],rc[N],s[N],c[N],ct,rt[N],o,n,m,f[2][N],ans;
struct no{
	int x,y;
}a[N];
int operator <(no x,no y){return x.x+x.y<y.x+y.y;}
void ins(int &o,int p,int l,int r,int x){
	o=++ct;
	s[o]=s[p];
	c[o]=c[p];
	if(l==r){
		s[o]+=l;
		c[o]++;
		return;
	}
	int md=(l+r)/2;
	if(x<=md){
		ins(lc[o],lc[p],l,md,x);
		rc[o]=rc[p];
	}
	else{
		ins(rc[o],rc[p],md+1,r,x);
		lc[o]=lc[p];
	}
	s[o]=s[lc[o]]+s[rc[o]];
	c[o]=c[lc[o]]+c[rc[o]];
}
void q(int o,int p,int l,int r,int x){
	if(l==r){
		ans+=-x*l;
		return;
	}
	int md=(l+r)/2;
	if(c[lc[o]]-c[lc[p]]>c[rc[o]]+x-c[rc[p]]){
		ans+=s[rc[o]]-s[rc[p]];
		q(lc[o],lc[p],l,md,x+c[rc[o]]-c[rc[p]]);
	}
	else if(c[lc[o]]-c[lc[p]]<c[rc[o]]+x-c[rc[p]]){
		ans+=-s[lc[o]]+s[lc[p]];
		q(rc[o],rc[p],md+1,r,x-c[lc[o]]+c[lc[p]]);
	}
	else ans+=s[rc[o]]-s[rc[p]]-s[lc[o]]+s[lc[p]];
}
void dfs(int l,int r,int a,int b){
	int md=(l+r)/2;
	if(l>r)return;
	int p;
	for(int j=a;j<=min(md-1,b);j++){
		ans=0;
		q(rt[md],rt[j],1,2000000002,0);
		if(f[o][md]>f[1-o][j]+ans){
			f[o][md]=f[1-o][j]+ans;
			p=j;
		}
	}
	dfs(l,md-1,a,p);
	dfs(md+1,r,p,b);
}
signed main(){
	freopen("shoes.in","r",stdin);
	freopen("shoes.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i].x>>a[i].y;
		a[i].x+=1000000001;
		a[i].y+=1000000001;
		if(a[i].x>a[i].y)swap(a[i].x,a[i].y);
	}
	sort(a+1,a+n+1);
	for(int i=1;i<=n;i++){
		ins(rt[i],rt[i-1],1,2000000002,a[i].x);
		ins(rt[i],rt[i],1,2000000002,a[i].y);
	}
	memset(f,127,sizeof(f));
	f[0][0]=0;
	for(int i=1;i<=m;i++){
		o=!o;
		for(int j=1;j<=n;j++)
			f[o][j]=1e17;
		dfs(i,n,i-1,n);
	}
	cout<<f[o][n];
}

14.lg6932
和loj535异常相似
类似14.1一样构造单调点集,则关于每个左下角对应的右上角是单调的。
用分治法求解,时间复杂度(O(nlog_2^2n))
注意细节,在更新时有一维满足要求即可。

#include<bits/stdc++.h>
#define int long long
using namespace std;
#define N 500010
struct no{
	int x,y;
}a[N],b[N];
int operator <(no x,no y){
	return x.x<y.x||(x.x==y.x&&x.y<y.y);
} 
int n,m,i1[N],i2[N],t1=1,t2=1,aa;
void fz(int l,int r,int L,int R){
	int ans=L,va=-1e18,md=(l+r)/2;
	for(int i=L;i<=R;i++)
		if(b[i2[i]].x>=a[i1[md]].x){
			int v=(b[i2[i]].x-a[i1[md]].x)*(b[i2[i]].y-a[i1[md]].y);
			if(v>va){
				ans=i;
				va=v;
				aa=max(aa,v);
			}
		}
	if(l==r)
		return;
	fz(l,md,L,ans);
	fz(md+1,r,ans,R);
}
signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%lld%lld",&a[i].x,&a[i].y);
	sort(a+1,a+n+1);
	i1[1]=1;
	for(int i=2;i<=n;i++){
		if(a[i].y<a[i1[t1]].y)
			i1[++t1]=i;
	}
	for(int i=1;i<=m;i++)
		scanf("%lld%lld",&b[i].x,&b[i].y);
	sort(b+1,b+m+1);
	i2[1]=m;
	for(int i=m-1;i;i--)
		if(b[i].y>b[i2[t2]].y)
			i2[++t2]=i;
	reverse(i2+1,i2+t2+1);
	fz(1,t1,1,t2);
	printf("%lld
",aa);
}

14.1 loj535
原题JOI final 2013 E
第一部分:最优交换次数是逆序对数。
时间复杂度(O(n^3))
第二部分:用容斥,求出交换一次后减少最多的逆序对。
((i,a_i))投射到平面上,则交换(a_i,a_j)一次后减少的是以((i,a_i))为左上角,((j,a_j))为右下角的矩形内的点的个数。
第三部分:构造单调点集。
如果存在两个点((j,a_j),(k,a_k),j<k,a_j<a_k),则(j)构建矩形显然是没有(k)优的。
单调点集的求法:维护栈,按照横坐标从小到大排序,如果当前点的纵坐标也单调则插入栈。
第四部分:决策单调性
一个矩形左上角对应的最优右下角是单调的,而且由于已经构造了单调点集,一个矩形内包含的点数就是在bit上查询(a_i,a_j)的值。
用分治和前面所提到的类似莫队的方法维护,时间复杂度(O(nlog_2^2n))
第五部分:逆向思维
更为优秀的做法:把所有单调点集按照横坐标排序,枚举左上角标号为(i),右下角标号为(j)的点,投射到平面((i,j))
中间一个点会对平面上的一个矩形的点的逆序对数作区间加法。
用扫描线+线段树维护矩形,时间复杂度(O(nlog_2n))
15.lg6918
(d1_i)表示(s->i)的距离,(d2_i)表示(i->s)的距离,可以在原图/反图上用dij求出。
点集的代价就是(sum_{iin S}sum_{jin S}[i eq j]d1_i+d2_j)
每个元素被计算(card(S)-1)次,所以代价是(sum_{iin S}(card(S)-1)(d1_i+d2_i))
根据排序不等式,(d1_i+d2_i)越大,对应的集合大小就越小
(a_i=d1_i+d2_i)从小到大排序,选择的一定是连续的区间。
考虑dp,设(f_{c,i})表示划分了(c)个,包含排序后前(i)个元素,有转移(f_{c,i}=min(f_{c-1,j}+(i-j)(s_i-s_j)))(s)(a)的前缀和数组
容易知道是正确的。
发现这个dp有决策单调性,于是能用二维决策单调性优化。
16.lg5897
(C)(R)差一个数量级,考虑对(C)建立线段树。
肯定要枚举([l,r]),我们要快速知道(l,r)中间的决策点(md)
设函数(p(l,r))表示([l,r])的决策点,则(p(l-1,r)leq p(l,r)leq p(l,r+1))
这是因为如果决策点交叉,则调整一下更优。
用二维决策单调性求出(p)
然而这样子在题目的空间下会MLE。
考虑阈值法,对线段树上面的节点暴力不优,考虑对下面的节点暴力。
考虑如何暴力,设(f_i)表示转移到当前节点的距离最小值,用滚动数组。
(g_i)表示原数组,则(f_i=min(g_i+|dis_i-dis_j|))
拆开绝对值后维护最小值即可。
17.数据分块鸡
思维量主要在费用函数的计算上。
我们希望在(O(log_2n))时间或者更低计算费用函数。
设当前分块区间([L,R]),查询是([l,r]),设分块区间中点为(md)
1.询问是([L,R])的真子集,(Lleq lleq rleq r),代价为((R-L)+l-r)
2.([L,R])是询问的真子集,则(lleq L)(Rleq r),代价为(1)
3.询问和([L,R])交于一个前缀,且前缀长度不超过([L,R])的一半,代价为(r-(L))
4.询问和([L,R])交于一个前缀,且前缀长度超过([L,R])的一半,代价为((R)-r)
5.询问和([L,R])交于一个后缀,且后缀长度不超过([L,R])的一半,代价为((R)-l)
6.询问和([L,R])交于一个后缀,且后缀长度超过([L,R])的一半,代价为(l-(L))
当分块区间和查询重合时,贡献会被计算(2)次,但是计算代价一次是(0)一次是(1),所以也不会影响答案。
所有情况事实上相当于平面上的一个矩形,要知道点的数量,横/纵坐标的和。
显然可以用可持久化线段树维护
18.loj6039
(C)非常小。
如果所有的(C)相同怎么做?肯定全选择最大的。
(f_{i,j})表示费用为(i),选(j)个的最大答案,则显然(f_i)是下凸函数。
如果有不同的(C),可以把所有物品按照(C)分组,然后把每个下凸函数合并。
枚举当前的数的模(C)的值,然后对每个模意义下相等的物品决策单调性。
下凸函数合并可以决策单调性:我们要求(c_{i+j}=max(a_i+b_j))(a_i)对应的最优(b_j)是单调的。
用经典的分治法合并即可。
边界很玄学

#include<bits/stdc++.h>
using namespace std;
#define N 1000010
#define int long long
int n,k,mx,nw[N],la[N],s2[N],t1,s1[N],t2;
vector<int>g[N];
void fz(int l,int r,int L,int R,int po,int v){
	int ans,va=-1e18,md=(l+r)/2;
	for(int i=L;i<=min(md,R);i++)
		if(s1[i]+s2[md-i]>va){
			va=s1[i]+s2[md-i];
			ans=i;
		}
	nw[md*v+po]=max(nw[md*v+po],va);
	if(l>=r)
		return;
	fz(l,md-1,L,ans,po,v);
	fz(md+1,r,ans,R,po,v);
}
signed main(){
	//freopen("jewelry.in","r",stdin);
	//freopen("jewelry.out","w",stdout);
	scanf("%lld%lld",&n,&k);
	for(int i=1;i<=n;i++){
		int c,v;
		scanf("%lld%lld",&c,&v);
		g[c].push_back(v);
		mx=max(mx,c); 
	}
	for(int i=1;i<=mx;i++){
		sort(g[i].begin(),g[i].end());
		reverse(g[i].begin(),g[i].end());
		for(int j=1;j<g[i].size();j++)
			g[i][j]+=g[i][j-1];
	}
	for(int j=0;j<=k;j++)
		nw[j]=la[j]=-1e18;
	nw[0]=0;
	for(int i=1;i<=mx;i++){
		for(int j=0;j<=k;j++)
			la[j]=nw[j];
		for(int j=0;j<=k;j++)
			nw[j]=-1e18;
		for(int j=0;j<i;j++){
			t1=t2=s1[0]=s2[0]=0;
			for(int l=j;l<=k;l+=i)
				s1[t1++]=la[l];
			s2[0]=0;
			t2++;
			for(int l=0;l<g[i].size();l++)
				s2[t2++]=g[i][l];
			for(int l=t2;l<t1;l++)
				s2[l]=s2[l-1];
			fz(0,t1-1,0,t1-1,j,i);
		}
	}
	for(int i=1;i<=k;i++){
		nw[i]=max(nw[i],nw[i-1]);
		printf("%lld ",nw[i]);
	}
}

这道题中如果(a)(b)合并能够决策单调性,则(a,b)只要一个下凸即可。

总结:
决策单调性用于求一个有序的数列。
分治法的函数移动是连续的。
二分+队列法可以支持向末尾插入元素/查询最优解,但是要求代价函数能够被快速算出。
只要dp方程满足四边形不等式通常就能决策单调性。
满足决策单调性的方程通常可以带权二分。
带环的决策单调性可以用分治解决。
两个凹函数/一个凹一个非凹函数的((max,+))卷积可以用决策单调性快速计算。
事实上,决策单调性不重要,列出dp方程,计算代价函数才是重要的。

原文地址:https://www.cnblogs.com/ctmlpfs/p/14725830.html