[正睿集训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卷积
\(\tt max\) 卷积就是形如下面式子:
一般的 \(\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-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\) 有关的式子即可。然后就是一个三维偏序问题用 \(\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\) 方程:
现在来证明上面的转移方程式决策单调的,那么就可以用单调队列优化了。
设 \(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(z\in[0,\infty))\) 单调不增,那么等价于它的导数小于 \(0\),我们分类讨论一下。
当 \(x\in[0,\infty)\) 时:
不难发现后面的一定更大,所以 \(h'(x)\leq 0\)
当 \(x\in(-\infty,0]\) 时,且 \(p\) 是偶数:
\(h(x)=x^p-(x+z)^p\)
和第一种情况同理,直接求导即可。
当 \(x\in(-\infty,-z)\),且 \(p\) 是奇数:
当 \(x\in[-z,0)\),且 \(p\) 是奇数:
那么所有情况我们都讨论完啦!证毕
现在来讲一讲怎么写决策单调性的题,主要是要利用决策点 \(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\) 段的最小费用,转移:
然后不难发现 \(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(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\) 的最小代价:
然后考虑下面的式子显然成立,所以直接用决策单调性即可: