[正睿集训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) 然后不等式合并。
只要我们证明四边形不等式就可以用决策单调性了,也就是说四边形不等式是决策单调性的充分条件,但是你推不出来怎么办呢?其实打表是一种好方法,四边形不等式还有下面一种等价形式:
具体应用可以看下面例题的 [NOI2009]诗人小G
max卷积
( t max) 卷积就是形如下面式子:
一般的 ( t max) 卷积是无法优化的,只能 (O(n^2)) 做。但如果 (a,b) 都是凸函数,那么可以直接 (O(n)) 合并,可以用 ( t two-pointers) 来维护,每次就贪心地选增量最大的就行了。
如果 (a) 是凸函数 (b) 没有特殊限制,那么可以用单调性优化 (dp) 来做,时间复杂度 (O(nlog n))
这个东西暂时还没有例题
篮球
题目描述
有 (n) 种物品,第 (i) 种物品有 (c_i) 个,初始代价是 (a_i),被选一次代价就会减少 (b_i)(第 (k) 次选的代价就是 (a_i-(k-1)b_i)),你想知道对于 (k=1...m),总共选 (k) 件物品代价的最小值是多少。
(nleq 1000,mleq10000)
解法
首先不难发现一个结论:至多有一种物品是部分选,其他物品要么全选或者全不选
考虑暴力 (dp),枚举这个不被全选的是谁,然后前缀后缀分别做背包,算出恰好 (k) 次的最小代价,最后再和这个不被全选的合并。但是暴力的复杂度是 (O(nm^2))
考虑分治优化,设 (dp(l,r)) 表示计算出了一个 (dp) 数组,且这个不被全选在 ([l,r]) 中的答案。那么每次取 (mid),递归左半边就可以算出 (dp(l,mid)),然后把右半边的一个一个加进去就行,右半边同理。然后两边算出来的 (dp) 数组取 ( t min) 就可以了。
每一层都是一个 (01) 背包,总复杂度 (O(nmlog n))
序列
题目描述
有 (n) 种物品,第 (i) 种物品第一次选择的收益是 (a_i),之后每次选择的收益都是 (b_i),代价始终为 (c_i),你需要求出总代价不超过 (m) 下收益的最大值。有 (q) 次修改,第 (j) 次修改会在第 (i) 次修改的基础上修改一个物品的两类收益,对于每次修改都要输出答案。
(n,m,Qleq 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)mlog n))
[HDU 6800] Play osu! on Your Tablet
题目描述
一个二维点序列 ((a_i,b_i)),你需要把它拆成两个子序列,使得两个子序列相邻两项的 曼哈顿距离 最小。
(nleq 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)),就只有这一种转移方式。
-
( t otherwise),(f(i,i-1)=f(i-1,k)+dis(k,i))
第一个式子是整体平移较为简单,我们想对第二个式子下手。第一种思路是:将 (dp) 式继续展开看有没有什么特殊性质 。所以我们看看展开 (f(i-1,k)) 后会是什么样子:
如果我们将 (f(i-2,k)) 继续展开,那么得到的也是类似的结果,所以转移可以写成:
设 (g(i)=f(i,i-1)),那么 (g) 的转移就只和自己有关系了,且我们可以建一个虚点 (n+1),所以不难发现最后的答案也是很容易用 (g) 来算的,那么只需要考虑 (g) 的转移就行了,设 (s_i=sum_{i=1}^{i-1}dis(i,i+1)),那么重新写一下转移方程:
下面介绍另一种思路:高维 (dp) 转低维 ,考虑枚举一段后缀是同一个子序列,也能推出上面的式子。
现在就来优化 (g(i)) 的转移吧,考虑只有 (dis(k-1,i)) 这个东西难做一点,写成曼哈顿距离实际上是 (|x_{k-1}-x_i|+|y_{k-1}-y_i|),所以可以分四种情况讨论,拆分成之和 (k) 有关的式子和之和 (i) 有关的式子即可。然后就是一个三维偏序问题用 ( t cdq) 分治即可解决(还有一维偏序关系是下标)
[HDU 6566] The Hanged Man
题目描述
(n) 个点的树,每个点有两种权值 (a_i,b_i),你需要选出一个独立集(里面的点两两不相邻),使得 (a_i) 的总和恰好为 (m),且 (b_i) 的总和尽量大,同时你需要计算这样独立集的数量。
(nleq100,mleq5000)
解法
暴力 (dp) 是 (O(nm^2)),多说无益,我们要让复杂度往 (n) 那边倾斜。
正解是 点分树优化dp ,先建出原图的点分树,我们主要想利用的性质是:点分树的深度不超过 (O(log n)),并且和一个点相邻的点要么是它点分树上的祖先,要么是它子树内的节点。
那么我们就按 (dfs) 序来一个个加入每个点,就考虑一个点选还是不选来转移,但是为了满足独立集这个条件,我们暴力记录下一个点祖先的状态,只要保证如果选出来的话它和它的祖先不相邻即可。
转移需要讨论一下这个点和上一个点的位置关系,这个自己 (yy) 一下就好啦,时间复杂度 (O(n^2m))
小D与随机
题目描述
大爱 (wzy) 鸽鸽,这讲得也太好了吧!
有一棵 (n) 个节点的有根树,随机生成一个排列 (p) 表示树上每个点的权值。定义一个点为好点当且仅当这个点的权值比所有祖先的权值大,求 (k) 的好点个数次方的期望。乘以 (n!) 后对 (998244353) 取模。
(nleq 5000)
解法
看到这种树上大小关系的题一定要想:例四,就是 (dp) 维护容斥系数。
把好点涂黑,剩下的叫白点,那么把限制用大小转移表示:
- 白点要比第一个黑点祖先小
- 黑点要比第一个黑点祖先大
我们从大点连向小点,并称这样的点为正向边,设 (f(i,j)) 表示子树 (i) 里面有 (j) 的点连到上面去了(能通过外向树走到上面,方便统计大小),先求出所有儿子的 (f) 卷积 (g(j))(这里期望可以直接相乘),接下来考虑:
- 如果 (i) 是黑点,那么 (i) 一定是 (j) 个点中最小的,这对应了一个概率 (frac{1}{j+1}),那么这么转移 (f(i,j+1)+=g(j) imesfrac{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) 方程:
现在来证明上面的转移方程式决策单调的,那么就可以用单调队列优化了。
设 (w(j,i)=|sum(i)-sum(j)-1-L|^p),我们只需要证明这个不等式:
设 (u=sum(j)-sum(j)-1-L,v=sum(i)-sum(j+1)-1-L),那么就是要证明这个东西:
所以我们只需要证明函数 (h(x)=|x|^p-|x+z|^p(zin[0,infty))) 单调不增,那么等价于它的导数小于 (0),我们分类讨论一下。
当 (xin[0,infty)) 时:
不难发现后面的一定更大,所以 (h'(x)leq 0)
当 (xin(-infty,0]) 时,且 (p) 是偶数:
(h(x)=x^p-(x+z)^p)
和第一种情况同理,直接求导即可。
当 (xin(-infty,-z)),且 (p) 是奇数:
当 (xin[-z,0)),且 (p) 是奇数:
那么所有情况我们都讨论完啦!证毕
现在来讲一讲怎么写决策单调性的题,主要是要利用决策点 (p_{i-1}leq p_i) 的条件,那么每个点一定对应了一段区间,它作为这一段区间的决策点。
所以我们维护一个决策点的单调队列,假设现在要处理 (i) 的转移,我们想要让队首就是 (i) 的最优决策点,可以一个二分函数计算一个决策点最远到哪里比另一个决策点优,就很容易判断第二个点能不能弹出队首了。
然后考虑怎么插入,还是用最远管辖点来判断,如果队尾在还没有起作用的时候就已经比 (i) 劣了,那么队尾就可以直接扔掉了,总时间复杂度 (O(nlog n))
CF868F
题目描述
解法
首先写出暴力 (dp) 方程,设 (f(i,j)) 表示前 (i) 个位置划分成 (j) 段的最小费用,转移:
然后不难发现 (w) 满足四边形不等式,因为 (C(x,2)) 增长得越来越快:
但是好像套诗人小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(nlog n))
[八省联考2018] 林克卡特树
题目描述
解法
这个题怎么证凸函数啊?还有这个题意转化也是神奇得一匹
事实上就是选 (k+1) 条点不交的链使得经过的边权和最大。
(wqs) 二分之后需要考虑链任意选,但是选一条链需要花费 (mid) 的代价怎么做。
令 (f(i,0/1/2)) 表示点 (i) 下面有 (0/1/2) 条边在链上。转移由于要记录下用了多少链所以我们把 (dp) 数组开成一个 (pair) 会好写很多,(f(i,0)) 回溯的时候可以存最大值,第二个细节是需要尽可能多的选取链,单点也算一条链。
于是总复杂度 (O(nlog 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
",ans);
}
yet another ioi problem
题目描述
一条链长度为 (L),有 (n) 个位置上有村庄,你需要选定恰好 (k) 个位置摆放水井,使得这 (n) 个村庄到最近水井的距离和最短。
(1leq n,kleq 1e5,Lleq 1e9)
解法
又要做题意转化了,因为要判断哪个水井是最近的有点麻烦。我们考虑如果只有一个水井那么一定是放在中位数的位置,我们不妨转化成把原序列划分成 (k) 段,每一段的水井都放在中位数的地方并且强制走到这个水井,由于可以用 (dp) 解决这个最优化问题所以这个题意转化是对的。
看到 恰好k个位置
这类表述一定要想到 (wqs) 二分。可以感知一下距离和是随 (k) 单调递减的,而且减少得越来越慢,所以可以去掉恰好 (k) 段这个限制条件。设 (f(i)) 表示划分到 (i) 的最小代价:
然后考虑下面的式子显然成立,所以直接用决策单调性即可: