Flying2018
2021-05-10 13:37:29
关于 wqs 二分部分可以参考 洛谷日报 或者 原论文,基础部分这里略过。
前半部分基本上是对
wqs 二分的本质是二分斜率,寻找切点。假设希望求出值的横坐标为
红线是切线,黄线是
不过对于一般的 wqs 二分问题,只要知道切线的截距和斜率,可以很容易求出任何切线上的点的坐标。但是有些情况下需要进行构造,上述做法就不太行了。
一种方法是随机扰动,这种方法在出题人精心构造的情况下复杂度会退化为指数级。
我们需要一种不导致复杂度退化的方法。显然的一点是,任意一条切线切在凸包上的点,其横坐标一定构成一个区间。而我们知道,只要最终
显然对于 wqs 二分能处理的大部分问题,其 dp 过程中的答案仍然是凸的。实际上对于序列问题,我们 dp 的就是每一个前缀的切点,每个切点的坐标是由前面的切点转移过来的。
那么现在所以对于每一个 dp 的子问题,我们都记录切线所切的区间。这样我们可以从最后开始,每次尝试找到上一个可行的转移点(可行指的是
当然,我们大可以不必转移整个区间。既然我们已经知道了它是一个区间,那么我们直接转移其最大值与最小值即可:
如上图,红点对应的转移可以直接求出,由此可以推出绿点(要求的解)的转移。
不妨用
转化后题意:构造一组方案,将长度为
使用上述做法就可以构造出一组可行解。
当然这道题还有另一种构造的方法,但是与本文无关。
事实上要严格证明一个函数是凸的需要不少时间,比如经典的 wqs 二分问题:
给定长度为
其实直接证明它的凸性并不容易,或者说并没有那么显然。
但是我们知道的一点是,费用流模型的函数一定是凸的。这里费用流函数
也就是说,如果我们能够将上述问题建出流量为
而上述问题很容易建出费用流模型:
自然就证明了答案是一个凸性的函数。
事实上,大部分 wqs 二分的问题都可以建出费用流模型(当然复杂度不一定正确)。
举个例子:
事实上这题相当于点有限流,每次可以流任意一条简单路径。
那费用流模型也很显然,直接将每个点拆成入点和出点,一条树边对应出点向入点连边。类似于上图的形式(不过扩展到了树上而已)
然后源点向每个点,每个点向汇点分别连
然后就可以套上 wqs 二分了,之后 dp 内容不是本文重点。
标准定义:
似乎有
举个例子:
需要将上述两个凸包做闵可夫斯基和。可以直接将其中一个凸包的每个顶点换成另一个凸包,然后保留外层凸包:
可以发现凸包上只有
事实上可以证明的是,从一个凸包跳到另一个凸包的线(即绿色部分)至多只有
下凸壳就是凸包留下下半部分的点集,可以看成被下方平行光照射到的部分。
下凸壳闵可夫斯基和当然可以仿照凸包的写法。但是这里有一个更方便的写法:
考虑下凸壳的一个性质:即任意
把这些点全部描述出来,我们完全可以用一个
如果我们求出它的差分数组,由于原数组是一个凸包,所以是单调不降的。
考虑求闵可夫斯基和的过程,本质上就是将两个差分数组归并排序一下。
演示:
当然我们并不需要真的求出差分数组,可以直接用两个指针指向需要合并的凸包,每次将斜率较大的点加入新的凸包即可。复杂度
代码十分简洁:
#define S(a) ((int)a.size())
typedef long long ll;
typedef vector<ll> vec;
vec operator +(const vec &a,const vec &b)
{
if(!S(a) || !S(b)) return vec();
vec c;c.reserve(S(a)+S(b));
c.push_back(a[0]+b[0]);
int p=1,q=1;ll w=a[0]+b[0];
while(p<S(a) || q<S(b))
{
if(q>=S(b) || (p<S(a) && a[p-1]-a[p]>b[q-1]-b[q])) c.push_back(w+=a[p]-a[p-1]),p++;
else c.push_back(w+=b[q]-b[q-1]),q++;
}
return c;
}
同样,通过这种记录的方式也可以很容易处理下凸壳的和(即两个下凸壳的并)。由于我们已经证明了取并后结果一定是一个下凸壳,故直接将每个
可以观察到,wqs 二分的函数图像一定是一个上凸壳/下凸壳,而横坐标的值域
观察朴素的 dp 转移方程,其中
如果将上面那道 wqs 二分的题目改成多组询问,即:
给定长度为
这样 wqs 二分就不太可做了,考虑用闵可夫斯基和处理。
具体来说,先考虑一种 naive 的 dp:令
为什么要加上后两维呢?因为我们处理区间合并的时候其实是可以合并中间的区间的,即:
直接这样做当然是
所以我们任取一个
所以仿照归并排序的思想,我们取
给定一颗大小为
既然是匹配,显然可以转化为费用流模型,故答案关于
考虑如果只对某一个
那么对于所有
考虑仿照 树上背包分治NTT 做法,我们对原树轻重链剖分,然后对于轻链直接暴力合并,复杂度
对于重链,先合并所有轻链的结果,然后做同上面那道题的分治处理,复杂度
事实上仿照 树上背包分治NTT 的思路按权分治做到
关键代码:
void dfs1(int u,int p)
{
fa[u]=p;siz[u]=1;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(v==p) continue;
fw[v]=w[i];
dfs1(v,u);siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
struct matr{
vec g[2][2];//g[0/1][0/1]:左边/右边 是否强制不匹配
//g[0][x]>=g[1][x] , g[x][0]>=g[x][1]
vec* operator [](int a){return g[a];}
matr(){}
matr(int u){g[0][0]=f[u][0];g[0][1]=g[1][0]=f[u][1];}
};
matr merge(const matr &a,const matr &b,int w)
{
matr c;
For01(X) For01(x) For01(y) For01(Y)
if(x && y) c[X][Y]=max(c[X][Y],inc(a.g[X][x]+b.g[y][Y],1,w));
else c[X][Y]=max(c[X][Y],a.g[X][x]+b.g[y][Y]);
return c;
}
matr solve(int l,int r,vec &g)
{
if(l==r) return matr(g[l]);
int s=0,mid=l;
for(int i=l;i<=r;i++) s+=f[g[i]][0].size();
int w=f[g[l]][0].size();
while(mid<r-1 && w*2<=s) w+=f[g[++mid]][0].size();
return merge(solve(l,mid,g),solve(mid+1,r,g),fw[g[mid]]);
}
vec g[N];
void dfs2(int u,int top)
{
f[u][0].pb(0);f[u][1].pb(0);
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(v==son[u] || v==fa[u]) continue;
dfs2(v,v);
}
if(u==1) return;
if(son[u]) dfs2(son[u],top);
g[top].push_back(u);
if(u!=top) return;
auto s=solve(0,S(g[top])-1,g[top]);
int p=fa[u];
f[p][0]=max(f[p][0]+s[0][0],inc(f[p][1]+s[0][1],1,fw[u]));
f[p][1]=f[p][1]+s[0][0];
g[top].clear();f[u][0].clear();f[u][1].clear();
}
给定长度为
没想到吧居然是同一场比赛的。
考虑线段树分治,然后保留分治过程每一个子区间的凸包。
一个显然的想法是得到每次询问的
由于最后的答案是凸的,故可以 wqs 二分额外价值
可以发现的是,线段树上总点数只有
假如每次询问的斜率
那么一个优化就是询问离线,然后找到每个询问需要查询的斜率,按该斜率排序,每一次 reset 凸包的指针位置,每次查询仿照上述均摊的做法。
总二分次数是
总时间复杂度
关键代码:
void solve(int u,int l,int r,int L,int R,int o)
{
if(L<=l && r<=R)
{
node h[2]={node(),node()};
For01(x) For01(y)
{
int &p=pos[u][x][y];
for(;p && f[u][x][y][p]-f[u][x][y][p-1]<o;p--);
ll v=f[u][x][y][p];
h[y]=max(h[y],g[0]+node(v-1ll*o*(p+(x||y)),p+(x||y)));
h[y]=max(h[y],g[1]+node(v-1ll*o*(p+(x||y)-x),p+(x||y)-x));
}
g[0]=h[0];g[1]=h[1];
return;
}
int mid=(l+r)>>1;
if(L<=mid) solve(u<<1,l,mid,L,R,o);
if(R>mid) solve(u<<1|1,mid+1,r,L,R,o);
}
for(int _=50;_;_--)
{
clear(1,1,n);
sort(q+1,q+m+1,[&](ques a,ques b){return (a.l+a.r)/2<(b.l+b.r)/2;});
for(int i=1;i<=m;i++)
if(q[i].l<=q[i].r)
{
int mid=(q[i].l+q[i].r)>>1;
g[0]=node(0,0);g[1]=node();
solve(1,1,n,l[q[i].u],r[q[i].u],mid);
node t=max(g[0],g[1]);
if(t.u>=w[q[i].u]) q[i].l=mid+1,res[q[i].u]=t.v+1ll*mid*w[q[i].u];
else q[i].r=mid-1;
if(t.u==w[q[i].u]) q[i].r=mid;
}
}