• 决策单调性优化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方程,计算代价函数才是重要的。

  • 相关阅读:
    Go之运算符
    前端开发之工具库
    MVC与MVVM
    开发工具之Vscode编辑器
    常用名词汇总
    python常见错误总结
    Python之常用第三方库总结
    PHP程序员的成长路线
    web 应用常见安全漏洞
    redis和memcached的区别详解
  • 原文地址:https://www.cnblogs.com/ctmlpfs/p/14725830.html
Copyright © 2020-2023  润新知