[正睿集训2021] dp优化

四边形不等式

现在我们有一个 \(dp\) 方程形如:\(f(i)=\min\{f(j)+w(j,i)\}\)

决策单调性的定义是对于 \(a<b<c<d\),若 \(f(c)\) 时从 \(b\) 转移比 \(a\) 转移优,那么对于 \(f(d)\) 也是从 \(b\) 转移比从 \(a\) 转移更优,即决策点位置单调。

如果 \(w\) 满足四边形不等式,那么 \(w(a,c)+w(b,d)\leq w(a,d)+w(b,c)\),如果四边形不等式成立,那么如果 \(f(a)+w(a,c)\geq f(b)+w(b,c)\),就有 \(f(a)+w(a,d)\geq f(b)+w(b,d)\),这就是决策单调性的数学语言。

上面的东西怎么证明呢?很简单,你把四边形不等式乘 \(-1\) 然后不等式合并。

只要我们证明四边形不等式就可以用决策单调性了,也就是说四边形不等式是决策单调性的充分条件,但是你推不出来怎么办呢?其实打表是一种好方法,四边形不等式还有下面一种等价形式:

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

具体应用可以看下面例题的 [NOI2009]诗人小G

max卷积

\(\tt max\) 卷积就是形如下面式子:

\[c_n=\max\{a_i+b_{n-i}\} \]

一般的 \(\tt max\) 卷积是无法优化的,只能 \(O(n^2)\) 做。但如果 \(a,b\) 都是凸函数,那么可以直接 \(O(n)\) 合并,可以用 \(\tt two-pointers\) 来维护,每次就贪心地选增量最大的就行了。

如果 \(a\) 是凸函数 \(b\) 没有特殊限制,那么可以用单调性优化 \(dp\) 来做,时间复杂度 \(O(n\log n)\)

这个东西暂时还没有例题

篮球

题目描述

\(n\) 种物品,第 \(i\) 种物品有 \(c_i\) 个,初始代价是 \(a_i\),被选一次代价就会减少 \(b_i\)(第 \(k\) 次选的代价就是 \(a_i-(k-1)b_i\)),你想知道对于 \(k=1...m\),总共选 \(k\) 件物品代价的最小值是多少。

\(n\leq 1000,m\leq10000\)

解法

首先不难发现一个结论:至多有一种物品是部分选,其他物品要么全选或者全不选

考虑暴力 \(dp\),枚举这个不被全选的是谁,然后前缀后缀分别做背包,算出恰好 \(k\) 次的最小代价,最后再和这个不被全选的合并。但是暴力的复杂度是 \(O(nm^2)\)

考虑分治优化,设 \(dp(l,r)\) 表示计算出了一个 \(dp\) 数组,且这个不被全选在 \([l,r]\) 中的答案。那么每次取 \(mid\),递归左半边就可以算出 \(dp(l,mid)\),然后把右半边的一个一个加进去就行,右半边同理。然后两边算出来的 \(dp\) 数组取 \(\tt min\) 就可以了。

每一层都是一个 \(01\) 背包,总复杂度 \(O(nm\log n)\)

序列

题目描述

\(n\) 种物品,第 \(i\) 种物品第一次选择的收益是 \(a_i\),之后每次选择的收益都是 \(b_i\),代价始终为 \(c_i\),你需要求出总代价不超过 \(m\) 下收益的最大值。有 \(q\) 次修改,第 \(j\) 次修改会在第 \(i\) 次修改的基础上修改一个物品的两类收益,对于每次修改都要输出答案。

\(n,m,Q\leq 2000\)

解法

先考虑没有修改怎么做,设 \(f(i,j)\) 为前 \(i\) 种物品选了 \(j\) 代价的最大收益,转移就先强制选一个 \(a_i\),然后选 \(b_i\) 的就是完全背包了。还有一种情况就是不操作,直接和 \(f(i-1,j)\) 取最大值。

考虑如何修改,修改一定是一个树形结构。对于每个物品我们先单独考虑,因为修改相当于是改一个子树内的 \(a_i,b_i\),也就是改一段 \(dfs\) 序区间的 \(a_i,b_i\),那么我们把这种修改离线下来,就能得到 \(dfs\) 序列上若干段区间 \(a_i,b_i\) 都是相同的。

考虑线段树分治,那么就只有加入操作了,由于加入操作最多打在线段树上 \(O((n+q)\log n)\) 次,然后一次加入操作的时间是 \(O(m)\) 的,所以总时间复杂度 \(O((n+q)m\log n)\)

[HDU 6800] Play osu! on Your Tablet

题目描述

一个二维点序列 \((a_i,b_i)\),你需要把它拆成两个子序列,使得两个子序列相邻两项的 曼哈顿距离 最小。

\(n\leq 1e5\)

解法

首先不难想到一个暴力 \(dp\),设 \(f(i,j)\) 为考虑到第 \(i\) 个,其中一个子序列的结尾是 \(i\),另一个子序列的结尾是 \(j\) 的最小代价,转移就从 \(f(i-1,...)\) 而来:

  • 如果 \(j<i-1\),那么 \(f(i,j)=f(i-1,j)+dis(i-1,i)\),就只有这一种转移方式。

  • \(\tt otherwise\)\(f(i,i-1)=f(i-1,k)+dis(k,i)\)

第一个式子是整体平移较为简单,我们想对第二个式子下手。第一种思路是:\(dp\) 式继续展开看有没有什么特殊性质 。所以我们看看展开 \(f(i-1,k)\) 后会是什么样子:

\[f(i,i-1)=\max\begin{cases}f(i-1,i-2)+dis(i-2,i)&k=i-2\\f(i-2,k)+dis(i-2,i-1)+dis(k,i)&k<i-2\end{cases} \]

如果我们将 \(f(i-2,k)\) 继续展开,那么得到的也是类似的结果,所以转移可以写成:

\[f(i,i-1)=\max_{k=1}^{i-1}\{f(k,k-1)+\sum_{j=k}^{i-2}dis(j,j+1)+dis(k-1,i)\} \]

\(g(i)=f(i,i-1)\),那么 \(g\) 的转移就只和自己有关系了,且我们可以建一个虚点 \(n+1\),所以不难发现最后的答案也是很容易用 \(g\) 来算的,那么只需要考虑 \(g\) 的转移就行了,设 \(s_i=\sum_{i=1}^{i-1}dis(i,i+1)\),那么重新写一下转移方程:

\[g(i)=\max_{k=1}^{i-1}\{g(k)-s_{k}+dis(k-1,i)\}+s_{i-1} \]

下面介绍另一种思路:高维 \(dp\) 转低维 ,考虑枚举一段后缀是同一个子序列,也能推出上面的式子。

现在就来优化 \(g(i)\) 的转移吧,考虑只有 \(dis(k-1,i)\) 这个东西难做一点,写成曼哈顿距离实际上是 \(|x_{k-1}-x_i|+|y_{k-1}-y_i|\),所以可以分四种情况讨论,拆分成之和 \(k\) 有关的式子和之和 \(i\) 有关的式子即可。然后就是一个三维偏序问题用 \(\tt cdq\) 分治即可解决(还有一维偏序关系是下标)

[HDU 6566] The Hanged Man

题目描述

\(n\) 个点的树,每个点有两种权值 \(a_i,b_i\),你需要选出一个独立集(里面的点两两不相邻),使得 \(a_i\) 的总和恰好为 \(m\),且 \(b_i\) 的总和尽量大,同时你需要计算这样独立集的数量。

\(n\leq100,m\leq5000\)

解法

暴力 \(dp\)\(O(nm^2)\),多说无益,我们要让复杂度往 \(n\) 那边倾斜。

正解是 点分树优化dp ,先建出原图的点分树,我们主要想利用的性质是:点分树的深度不超过 \(O(\log n)\),并且和一个点相邻的点要么是它点分树上的祖先,要么是它子树内的节点。

那么我们就按 \(dfs\) 序来一个个加入每个点,就考虑一个点选还是不选来转移,但是为了满足独立集这个条件,我们暴力记录下一个点祖先的状态,只要保证如果选出来的话它和它的祖先不相邻即可。

转移需要讨论一下这个点和上一个点的位置关系,这个自己 \(yy\) 一下就好啦,时间复杂度 \(O(n^2m)\)

小D与随机

题目描述

大爱 \(wzy\) 鸽鸽,这讲得也太好了吧!

有一棵 \(n\) 个节点的有根树,随机生成一个排列 \(p\) 表示树上每个点的权值。定义一个点为好点当且仅当这个点的权值比所有祖先的权值大,求 \(k\) 的好点个数次方的期望。乘以 \(n!\) 后对 \(998244353\) 取模。

\(n\leq 5000\)

解法

看到这种树上大小关系的题一定要想:例四,就是 \(dp\) 维护容斥系数。

把好点涂黑,剩下的叫白点,那么把限制用大小转移表示

  • 白点要比第一个黑点祖先小
  • 黑点要比第一个黑点祖先大

我们从大点连向小点,并称这样的点为正向边,设 \(f(i,j)\) 表示子树 \(i\) 里面有 \(j\) 的点连到上面去了(能通过外向树走到上面,方便统计大小),先求出所有儿子的 \(f\) 卷积 \(g(j)\)(这里期望可以直接相乘),接下来考虑:

  • 如果 \(i\) 是黑点,那么 \(i\) 一定是 \(j\) 个点中最小的,这对应了一个概率 \(\frac{1}{j+1}\),那么这么转移 \(f(i,j+1)+=g(j)\times\frac{k}{j+1}\)
  • 如果 \(i\) 是白点,那么就容斥这条边,如果断开那么 \(f(i,j)+=g(j)\),如果反向那么 \(f(i,j+1)-=g(j)\),因为子树大小为 \(1\) 所以就不用除啦。

根据原理:两个点只在他们的 \(lca\) 处对复杂度有 \(1\) 的贡献,那么总复杂度 \(O(n^2)\)

[NOI2009] 诗人小G

题目描述

点此看题

解法

\(sum(i)=i+\sum_{j=1}^ia_j\),那么有很显然的 \(dp\) 方程:

\[f(i)=\min\{f(j)+|sum(i)-sum(j)-1-L|^p\} \]

现在来证明上面的转移方程式决策单调的,那么就可以用单调队列优化了。

\(w(j,i)=|sum(i)-sum(j)-1-L|^p\),我们只需要证明这个不等式:

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

\(u=sum(j)-sum(j)-1-L,v=sum(i)-sum(j+1)-1-L\),那么就是要证明这个东西:

\[|u+1+a_{i+1}|^p+|v|^p\geq |u|^p+|v+1+a_{i+1}|^p \]

\[|v|^p-|v+1+a_{i+1}|^p\geq|u|^p-|u+1+a_{i+1}|^p \]

所以我们只需要证明函数 \(h(x)=|x|^p-|x+z|^p(z\in[0,\infty))\) 单调不增,那么等价于它的导数小于 \(0\),我们分类讨论一下。

\(x\in[0,\infty)\) 时:

\[h(x)=x^p-(x+z)^p \]

\[h'(x)=px^{p-1}-p(x+z)^{p-1} \]

不难发现后面的一定更大,所以 \(h'(x)\leq 0\)

\(x\in(-\infty,0]\) 时,且 \(p\) 是偶数:

\(h(x)=x^p-(x+z)^p\)

和第一种情况同理,直接求导即可。

\(x\in(-\infty,-z)\),且 \(p\) 是奇数:

\[h(x)=-x^p+(x+z)^p \]

\[h'(x)=-px^{p-1}+p(x+z)^{p-1}\leq 0 \]

\(x\in[-z,0)\),且 \(p\) 是奇数:

\[h(x)=-x^p-(x+z)^p \]

\[h'(x)=-px^{p-1}-p(x+z)^{p-1}\leq 0 \]

那么所有情况我们都讨论完啦!证毕


现在来讲一讲怎么写决策单调性的题,主要是要利用决策点 \(p_{i-1}\leq p_i\) 的条件,那么每个点一定对应了一段区间,它作为这一段区间的决策点。

所以我们维护一个决策点的单调队列,假设现在要处理 \(i\) 的转移,我们想要让队首就是 \(i\) 的最优决策点,可以一个二分函数计算一个决策点最远到哪里比另一个决策点优,就很容易判断第二个点能不能弹出队首了。

然后考虑怎么插入,还是用最远管辖点来判断,如果队尾在还没有起作用的时候就已经比 \(i\) 劣了,那么队尾就可以直接扔掉了,总时间复杂度 \(O(n\log n)\)

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 100005;
#define lb long double
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int T,n,m,p,hd,tl,a[M],o[M],L[M],R[M],ls[M];
char s[M][35];lb f[M];
lb qkpow(lb a,int b)
{
	lb r=1;
	while(b>0)
	{
		if(b&1) r=r*a;
		a=a*a;
		b>>=1;
	}
	return r;
}
lb Abs(lb x)
{
	return x>0?x:-x;
}
lb val(int j,int i)
{
	return f[j]+qkpow(Abs(a[i]-a[j]-1-m),p);
}
void divide(int i)
{
	int l=L[tl],r=R[tl],tmp=r+1;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(val(i,mid)<=val(o[tl],mid))
			tmp=mid,r=mid-1;
		else l=mid+1;
	}
	if(tmp!=L[tl]) R[tl]=tmp-1;else tl--;
	if(tmp<=n) o[++tl]=i,L[tl]=tmp,R[tl]=n;
}
void dfs(int x)
{
	int y=ls[x];if(y) dfs(y);
	for(int i=y+1;i<x;i++)
		printf("%s ",s[i]);
	printf("%s\n",s[x]);
}
void work()
{
	n=read();m=read();p=read();
	o[hd=tl=1]=0;L[1]=1;R[1]=n;
	for(int i=1;i<=n;i++)
	{
		scanf("%s",s[i]);
		a[i]=strlen(s[i])+a[i-1]+1;
	}
	for(int i=1;i<=n;i++)
	{
		while(hd<tl && R[hd]<i) hd++;L[hd]++;
		f[i]=val(o[hd],i);ls[i]=o[hd];
		while(hd<tl && L[hd]>R[hd]) hd++;
		while(hd<tl && val(i,L[tl])
		<=val(o[tl],L[tl])) tl--;
		divide(i);
	}
	if(f[n]>1e18)
	{
		puts("Too hard to arrange");
		return ;
	}
	printf("%lld\n",(long long)(f[n]+0.5));
	dfs(n);
}
signed main()
{
	T=read();
	while(T--)
	{
		work();
		puts("--------------------");
	}
}

CF868F

题目描述

点此看题

解法

首先写出暴力 \(dp\) 方程,设 \(f(i,j)\) 表示前 \(i\) 个位置划分成 \(j\) 段的最小费用,转移:

\[f(i,j)=\min\{f(k,j-1)+w(k+1,i))\} \]

然后不难发现 \(w\) 满足四边形不等式,因为 \(C(x,2)\) 增长得越来越快:

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

但是好像套诗人小G的做法不行,因为这道题的 \(w\) 不能 \(O(1)\) 算。这里讲一种崭新的做法,考虑分治,设函数 \(solve(l,r,L,R)\) 表示区间 \([l,r]\)\([L,R]\) 转移而来,每次我们找到 \(mid\) 的转移点 \(p\),分成 \(solve(l,mid-1,L,p)\)\(solve(mid+1,r,p,R)\) 递归下去。

那么 \(w\) 怎么算呢?用类似莫队的操作维护即可,时间复杂度为什么是对的呢?因为考虑一个函数的功能完成之后只需要花 \(O(r-l)\) 的时间就可以跳到兄弟函数,或者是返回上一层函数。那么总时间复杂度 \(O(n\log n)\)

[八省联考2018] 林克卡特树

题目描述

点此看题

解法

这个题怎么证凸函数啊?还有这个题意转化也是神奇得一匹

事实上就是选 \(k+1\)点不交的链使得经过的边权和最大。

\(wqs\) 二分之后需要考虑链任意选,但是选一条链需要花费 \(mid\) 的代价怎么做。

\(f(i,0/1/2)\) 表示点 \(i\) 下面有 \(0/1/2\) 条边在链上。转移由于要记录下用了多少链所以我们把 \(dp\) 数组开成一个 \(pair\) 会好写很多,\(f(i,0)\) 回溯的时候可以存最大值,第二个细节是需要尽可能多的选取链,单点也算一条链。

于是总复杂度 \(O(n\log n)\)我竟然写了代码

#include <cstdio>
#include <cstring>
const int M = 300005;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,k,up,mid,ans,tot,f[M];
struct edge
{
	int v,c,next;
}e[2*M];
struct node
{
	int x,y;
	node(int X=0,int Y=0) : x(X) , y(Y) {}
	node operator + (int r) {return node(x+r,y);}
	node operator + (node r) {return node(x+r.x,y+r.y);}
	bool operator < (node r) {return x==r.x?y<r.y:x<r.x;}
}dp[M][3];
int Abs(int x)
{
	return x>0?x:-x;
}
node max(node a,node b)
{
	return a<b?b:a;
}
node upd(node a)//新增一条链 
{
	return node(a.x-mid,a.y+1);
}
void dfs(int u,int fa)
{
	dp[u][2]=max(dp[u][2],upd(dp[u][2]));
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v,c=e[i].c;
		if(v==fa) continue;
		dfs(v,u);
		dp[u][2]=max(dp[u][2]+dp[v][0],upd(dp[u][1]+dp[v][1]+c));
		dp[u][1]=max(dp[u][1]+dp[v][0],dp[u][0]+dp[v][1]+c);
		dp[u][0]=dp[u][0]+dp[v][0];
	}
	dp[u][0]=max(dp[u][0],max(upd(dp[u][1]),dp[u][2]));
}
signed main()
{
	n=read();k=read()+1;
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read(),c=read();
		up+=Abs(c);
		e[++tot]=edge{v,c,f[u]},f[u]=tot;
		e[++tot]=edge{u,c,f[v]},f[v]=tot;
	}
	int l=-up,r=up;
	while(l<=r)
	{
		mid=(l+r)>>1;
		for(int i=1;i<=n;i++)
			dp[i][0]=dp[i][1]=dp[i][2]=node(0,0);
		dfs(1,0);
		if(dp[1][0].y>=k)
		{
			ans=dp[1][0].x+mid*k;
			l=mid+1;
		}
		else r=mid-1;
	}
	printf("%lld\n",ans);
}

yet another ioi problem

题目描述

一条链长度为 \(L\),有 \(n\) 个位置上有村庄,你需要选定恰好 \(k\) 个位置摆放水井,使得这 \(n\) 个村庄到最近水井的距离和最短。

\(1\leq n,k\leq 1e5,L\leq 1e9\)

解法

又要做题意转化了,因为要判断哪个水井是最近的有点麻烦。我们考虑如果只有一个水井那么一定是放在中位数的位置,我们不妨转化成把原序列划分成 \(k\) 段,每一段的水井都放在中位数的地方并且强制走到这个水井,由于可以用 \(dp\) 解决这个最优化问题所以这个题意转化是对的。

看到 恰好k个位置 这类表述一定要想到 \(wqs\) 二分。可以感知一下距离和是随 \(k\) 单调递减的,而且减少得越来越慢,所以可以去掉恰好 \(k\) 段这个限制条件。设 \(f(i)\) 表示划分到 \(i\) 的最小代价:

\[dp(i)=\min\{dp(j)+w(j+1,i)-k\} \]

然后考虑下面的式子显然成立,所以直接用决策单调性即可:

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

posted @ 2021-02-22 22:22  C202044zxy  阅读(423)  评论(0编辑  收藏  举报