WQS 二分学习笔记
一、算法介绍
\(\texttt{WQS}\) 二分又称带权二分、凸优化,因王钦石神仙最先提出而得名。
\(\texttt{WQS}\) 二分能够解决的问题类型:
有若干个物品可供选择,某些特殊物品限制恰好选 \(m\) 个,求最优方案。
前提条件:
- 记 \(f_i\) 为限制恰好选 \(i\) 个的答案,那么 \((i,f_i)\) 是凸函数。
- 去掉限制后可以快速计算答案。
凸性是根据题目性质分析出来的,我们事先并不知道凸包的形状,同时目标变为计算 \(f_m\) 。
\(\texttt{WQS}\) 二分干的事情是二分斜率,快速计算斜率为 \(k\) 的直线会切在凸包上的哪个点。
注意到 \((x,f_x)\) 对应的截距为 \(f_x-kx\) ,可以看成**将每个特殊物品的代价都减去 \(k\) **。
以上凸壳为例,求切点等价于求 \(f_x-kx\) 的最大值。
而这等价于将每个特殊物品的代价减去 \(k\) 后,计算没有特殊限制的答案。
对于大多数题目, \(f_x\) 为整数,那么斜率 \(\Delta_x=f_x-f_{x-1}\) 也为整数,使用整数二分就足够了。
只有当 \(f_x\) 为实数时,才需要使用实数二分。
二分边界应为斜率的最值,即 \(\min,\max\Delta_i\) 。
\(\texttt{WQS}\) 二分思路还算清晰,然而三点共线的细节非常搞心态。
换一种方式理解:目标找到使得切点横坐标 \(\ge m\) 的最大(上凸)或最小(下凸)斜率 \(k\) 。
正确操作:记二分到的切点为 \((x,f_x)\) ,在闭区间的一侧用 \(f_x+km\) (而不是 \(f_x+kx\) )更新答案。
如果直线切在凸包上的多个位置(凸包上某一段斜率与二分斜率相同),根据目标,我们需要返回横坐标最大的切点。
///上凸函数,切点在右边时应增大斜率
while(r-l>1)///左闭右开
{
int mid=(l+r)/2;
if(/**切点横坐标**/>=m) l=mid,res=/**切点纵坐标**/+mid*m;
else r=mid;
}
///下凸函数,切点在右边时应减小斜率
while(r-l>1)///左开右闭
{
int mid=(l+r)/2;
if(/**切点横坐标**/>=m) r=mid,res=/**切点纵坐标**/+mid*m;
else l=mid;
}
二、相关例题
例1、\(\texttt{P2619 [国家集训队]Tree I}\)
题目描述
给定一张 \(n\) 个点, \(m\) 条边的带权无向图,每条边为黑色或白色。
求一棵权值和最小且恰有 \(k\) 条白色边的最小生成树。
数据范围
- \(1\le n\le 5\cdot 10^4,1\le m\le 10^5,1\le w_i\le 100\)。
时间限制 \(\texttt{2s}\),空间限制$ \texttt{500MB}$ 。
分析
记 \(f_i\) 为恰有\(i\)条白色边的最小生成树权值之和,不存在为 \(\infty\) 。
关于 \(f_i\) 为下凸函数的证明:
\(f_0\) 可以看成仅用黑边跑 \(kruskal\) 算法(不存在的边权值视为 \(\infty\) ),那么 \(f_i\) 就是在 \(f_{i-1}\) 的基础上,加入一条白边,再从环上删掉一条权值最大的黑边。
如果第 \(i+1\) 次替换的增量 \(\Delta_{i+1}=f_{i+1}-f_i\) 比第 \(i\) 次的 \(\Delta_i=f_i-f_{i-1}\) 小,那么对于 \(f_i\) ,我们放弃第 \(i\) 次替换,执行第 \(i+1\) 次替换,容易发现这也是一棵合法的生成树。
但 \(f'_i=f_i-\Delta_i+\Delta_{i+1}\lt f_i\) ,这与 \(f_i\) 的最优性矛盾。
因此 \(\Delta_i\le\Delta_{i+1}\) ,即 \(f_i\) 为下凸函数。
二分斜率 \(mid\) ,将所有白边的权值减去 \(mid\) 后跑 \(kruskal\) ,切点横坐标为白边个数。
注意为了保证最优位置横坐标最大,我们要尽可能多的使用白边,在排序时边权相同的白边在前。
时间复杂度 \(\mathcal O((n+m)\log n\log V)\) 。
可以将黑边和白边分别排序后做归并,时间复杂度降至 \(\mathcal O((n+m)(\log n+\log V))\) ,但是我懒。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=1e5+5;
int k,m,n,res;
int f[maxn];
struct edge
{
int u,v,w,c;
}e[maxn];
bool cmp(edge a,edge b)
{
if(a.w!=b.w) return a.w<b.w;
return a.c<b.c;
}
int find(int x)
{
if(f[x]==x) return x;
return f[x]=find(f[x]);
}
pii calc(int x)
{
for(int i=0;i<n;i++) f[i]=i;
for(int i=1;i<=m;i++) if(!e[i].c) e[i].w-=x;
sort(e+1,e+m+1,cmp);
int cnt=0,val=0;
for(int i=1;i<=m;i++)
{
int u=find(e[i].u),v=find(e[i].v);
if(u==v) continue;
f[u]=v,cnt+=!e[i].c,val+=e[i].w;
}
for(int i=1;i<=m;i++) if(!e[i].c) e[i].w+=x;
return mp(cnt,val);
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++) scanf("%d%d%d%d",&e[i].u,&e[i].v,&e[i].w,&e[i].c);
int l=-101,r=100;
while(r-l>1)
{
int mid=(l+r)>>1;
pii cur=calc(mid);
if(cur.fi>=k) r=mid,res=cur.se+mid*k;
else l=mid;
}
printf("%d\n",res);
return 0;
}
例2、\(\text{P5633 最小度限制生成树}\)
题目描述
给定一张 \(n\) 个点, \(m\) 条边的带权无向图,要求 \(s\) 号点恰好连了 \(k\) 条边,求权值和最小的生成树,无解输出 Impossible
。
数据范围
- \(1\le n\le 5\cdot 10^4,1\le m\le 5\cdot 10^5,1\le k\le 100,0\le w\le 3\cdot 10^4\)。
时间限制 \(\texttt{8s}\) ,空间限制 \(\texttt{256MB}\) 。
分析
先判掉无解的情况:
- 原图不连通。
- 给边去重后, \(s\) 的出边数量 \(\lt k\) 。
- 加入所有与 \(s\) 无关的边后,连通块(不包含 \(\{s\}\) )数量 \(\gt k\) 。
记 \(f_k\) 为限制 \(s\) 恰好连接 \(k\) 条边的最小权值,不存在为 \(\infty\) 。
关于 \(f_k\) 为下凸函数的证明:
\(f_k\) 可以看成在 \(f_{k-1}\) 的基础上,加入一条从 \(s\) 出发的边,再从环上删掉一条与 \(s\) 无关的边。
记 \(\Delta_k=f_k-f_{k-1}\) ,如果 \(\Delta_k\gt\Delta_{k+1}\) ,我们放弃第 \(k\) 次替换,转而执行第 \(k+1\) 次替换,容易发现这仍然是一棵合法的 \(deg_s=k\) 的生成树,但与 \(f_k\) 的最小性矛盾。
因此 \(\Delta_k\le\Delta_{k+1}\) ,即 \(f_k\) 为下凸函数。
接下来套一个 \(\texttt{WQS}\) 二分板子就可以了。
本题数据范围较大,用归并排序实现,时间复杂度 \(\mathcal O((n+m)(\log n+\log V))\) 。
#include<bits/stdc++.h>
#define ll long long
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,ll>
using namespace std;
const int maxn=5e4+5;
int k,m,n,s;
ll res;
int f[maxn];
bitset<maxn> vis;
struct edge
{
int u,v,w;
};
vector<edge> v1,v2;
inline int read()
{
int q=0;char ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) q=10*q+ch-'0',ch=getchar();
return q;
}
bool cmp(edge a,edge b)
{
return a.w<b.w;
}
int find(int x)
{
if(f[x]==x) return x;
return f[x]=find(f[x]);
}
bool merge(int u,int v)
{
u=find(u),v=find(v);
if(u==v) return false;
return f[u]=v,true;
}
pii calc(int x)
{
static int a=v1.size(),b=v2.size();
for(int i=1;i<=n;i++) f[i]=i;
ll cnt=0,val=0;
for(int i=0,j=0;i<a||j<b;)
{
if(i<a&&(j>=b||v1[i].w-x<=v2[j].w))
{
if(merge(v1[i].u,v1[i].v)) cnt++,val+=v1[i].w-x;
i++;
}
else
{
if(merge(v2[j].u,v2[j].v)) val+=v2[j].w;
j++;
}
}
return mp(cnt,val);
}
int main()
{
n=read(),m=read(),s=read(),k=read();
for(int i=1;i<=n;i++) f[i]=i;
for(int i=1;i<=m;i++)
{
int u=read(),v=read(),w=read();
if(u==v) continue;
if(v==s) swap(u,v);
if(u==s) vis[v]=true,v1.push_back({u,v,w});
else merge(u,v),v2.push_back({u,v,w});
}
if(vis.count()<k) printf("Impossible\n"),exit(0);
vis.reset();
for(int i=1;i<=n;i++) if(i!=s) vis[find(i)]=true;
if(vis.count()>k) printf("Impossible\n"),exit(0);
for(auto p:v1) merge(p.u,p.v);
for(int i=1;i<=n;i++) if(find(i)!=find(1)) printf("Impossible\n"),exit(0);
sort(v1.begin(),v1.end(),cmp);
sort(v2.begin(),v2.end(),cmp);
int l=-3e4-1,r=3e4;
while(r-l>1)
{
int mid=(l+r)>>1;
pii cur=calc(mid);
if(cur.fi>=k) r=mid,res=cur.se+mid*k;
else l=mid;
}
printf("%lld\n",res);
return 0;
}
例3、\(\texttt{CF125E MST Company 题解}\)
参考题解。
题目描述
给定一张 \(n\) 个点, \(m\) 条边的带权无向图,保证图中没有重边、自环。
求一棵满足 \(deg_1=k\) 的边权和最小的生成树,输出方案,无解输出 -1
。
数据范围
- \(1\le n\le 5\cdot 10^3,0\le m\le 10^5,0\le k\le 5000,1\le w_i\le 10^5\)。
时间限制 \(\texttt{8s}\) ,空间限制 \(\texttt{256MB}\) 。
分析
容易发现本题其实比上一题就多了一个输出方案。
记从 \(1\) 出发的边为白边,其余为黑边。
网上常见的错解:将白边边权减去 \(r\) 后跑 \(kruskal\) ,跑到 \(k\) 条白边后忽略后面的所有黑边。
原因在于 \(\texttt{WQS}\) 二分仅能保证存在一种恰有 \(k\) 条白边的方案,但上述构造却指定了这 \(k\) 条白边,因此很容易让答案偏大或返回无解。
然而原题数据太水全放过了,下面是正确的构造方法:
先用最少白边的策略跑 \(kruskal\) ,固定所有白边,考虑在它的基础上构造方案。
再用最多白边的策略跑 \(kruskal\) ,记 \(lim_w\) 为尚未固定的边权大于 \(w\) 的白边数量最小值。
保留所有白边,对于第 \(i\) 层(边权为 \(i\) 的所有边),优先使用白边,直到满足 \(now+lim_i=k\) ,层内其他边再使用黑边。
这个做法的正确性在于,根据 \(kruskal\) 算法的性质,每一层连完以后连通块形状固定,而我们已经考虑了后面所有层的限制。
时间复杂度 \(\mathcal O((n+m)(\log n+\log V))\) 。
拍了几万组数据,应该没假。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=1e5+5;
int a,b,k,m,n,u,v,w,res;
int f[maxn];
bitset<maxn> vis;
map<int,int> lim;
struct edge
{
int u,v,w,id;
};
vector<edge> v1,v2;
bool cmp(edge a,edge b)
{
return a.w<b.w;
}
int find(int x)
{
if(f[x]==x) return x;
return f[x]=find(f[x]);
}
bool merge(int u,int v)
{
u=find(u),v=find(v);
if(u==v) return false;
return f[u]=v,true;
}
pii calc(int x)
{
for(int i=1;i<=n;i++) f[i]=i;
int cnt=0,val=0;
for(int i=0,j=0;i<a||j<b;)
{
if(i<a&&(j>=b||v1[i].w-x<=v2[j].w))
{
if(merge(v1[i].u,v1[i].v)) cnt++,val+=v1[i].w-x;
i++;
}
else
{
if(merge(v2[j].u,v2[j].v)) val+=v2[j].w;
j++;
}
}
return mp(cnt,val);
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
if(u==1||v==1) v1.push_back({u,v,w,i});
else merge(u,v),v2.push_back({u,v,w,i});
}
if(v1.size()<k) printf("-1\n"),exit(0);
for(int i=2;i<=n;i++) vis[find(i)]=1;
if(vis.count()>k) printf("-1\n"),exit(0);
for(auto p:v1) merge(p.u,p.v);
for(int i=2;i<=n;i++) if(find(i)!=find(1)) printf("-1\n"),exit(0);
sort(v1.begin(),v1.end(),cmp);
sort(v2.begin(),v2.end(),cmp);
a=v1.size(),b=v2.size(),vis.reset();
int l=-1e5-1,r=1e5;
while(r-l>1)
{
int mid=(l+r)>>1;
pii cur=calc(mid);
if(cur.fi>=k) r=mid,res=cur.se+mid*k;
else l=mid;
}
for(auto &p:v1) p.w-=r;
for(int i=1;i<=n;i++) f[i]=i;
int cur=0;///第一遍以最少白边跑kruskal,强制这些白边在最终方案出现,cur为最小白边数
for(int i=0,j=0;i<a||j<b;)
{
if(i<a&&(j>=b||v1[i].w<v2[j].w))
{
if(merge(v1[i].u,v1[i].v)) cur++,vis[v1[i].id]=1;
i++;
}
else merge(v2[j].u,v2[j].v),j++;
}
for(int i=1;i<=n;i++) f[i]=i;
for(auto p:v1) if(vis[p.id]) merge(p.u,p.v);
int now=0;///第二遍以最多白边跑kruskal,lim[w]为边权大于w的自由白边数量最小值
for(int i=0,j=0;i<a||j<b;)
{
static int val=0;
if(i<a&&(j>=b||v1[i].w<=v2[j].w)) now+=merge(v1[i].u,v1[i].v),val=v1[i++].w;
else merge(v2[j].u,v2[j].v),val=v2[j++].w;
lim[val]=now;
}
for(auto &p:lim) p.se=max(k-cur-p.se,0);
for(int i=1;i<=n;i++) f[i]=i;
for(auto p:v1) if(vis[p.id]) merge(p.u,p.v);
for(int i=0,j=0,now=cur;i<a||j<b;)
{///第三遍kruskal,在now+lim[w]<=k的限制下尽可能多选白边
if(i<a&&(j>=b||v1[i].w<=v2[j].w))
{
if(now+lim[v1[i].w]<k&&merge(v1[i].u,v1[i].v)) now++,vis[v1[i].id]=1;
i++;
}
else
{
if(merge(v2[j].u,v2[j].v)) vis[v2[j].id]=1;
j++;
}
}
printf("%d\n",n-1);
for(int i=1;i<=m;i++) if(vis[i]) printf("%d ",i);
putchar('\n');
return 0;
}
例4、\(\texttt{CF739E Gosha is hunting}\)
题目描述
有 \(n\) 个物品, \(a\) 个 \(\texttt{A}\) 类球和 \(b\) 个 \(\texttt{B}\) 类球, \(\texttt{A}\) 类球和 \(\texttt{B}\) 类球抓到第 \(i\) 个物品的概率分别为 \(p_i,q_i\) 。
每类球最多在同一个物品上使用一次,但一个物品可以同时使用两类球。求期望抓到物品个数的最大值,要求绝对或相对误差 \(\le10^{-4}\) 。
数据范围
- \(2\le n\le 2\cdot 10^3,0\le a,b\le n,0\le p_i,q_i\le 1\) 。
时间限制 \(\texttt{5s}\) ,空间限制 \(\texttt{256MB}\) 。
分析
容易想到一个 \(\mathcal O(n^3)\) 的 \(\texttt{dp}\) , \(f_{i,j,k}\) 表示考虑前 \(i\) 个物品,使用 \(j\) 个 \(\texttt{A}\) 类球和 \(k\) 个 \(\texttt{B}\) 类球的最大收益。
转移方程如下:
大胆猜测 \(f_{i,j,k}\) 关于 \(j\) 是上凸函数。
用 \(\texttt{WQS}\) 二分优化,相当于 \(\texttt{A}\) 类球数量不限,但每使用一次会产生 \(mid\) 的代价。
于是 \(\texttt{dp}\) 降维后复杂度变为 \(\mathcal O(n^2)\) 。
注意需要实数二分,时间复杂度 \(\mathcal O(n^2\log V)\) 。
本题凸性可以用费用流证明:
从源点向 \(\texttt{A,B}\) 类球分别连容量为 \(a,b\) 的边,从 \(\texttt{A,B}\) 类球向每个物品连容量为 \(1\) ,费用为 \(p_i,q_i\) 的边,再从每个物品向汇点连边 \((1,0),(1,-p_i\cdot q_i)\) ,跑最大费用最大流即可。
由于增广路长度非严格递减,因此代价关于流量是凸函数。
Warning:
- 二分套二分是假做法,因为 \(\texttt{WQS}\) 二分需要记录使用的 \(\texttt{A}\) 球数量最大值,而二分套二分需要记录在 \(\texttt{B}\) 球数量恰好为 \(b\) 的前提下,使用 \(\texttt{A}\) 球数量的最大值,但这个问题没有办法解决, \(hack\) 数据可以看这里。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2005;
const double eps=1e-9,inf=1e9;
int a,b,n;
double res,p[maxn],q[maxn];
struct node
{
double val;
int cnt;
};
node operator+(node a,node b)
{
return {a.val+b.val,a.cnt+b.cnt};
}
bool operator<(node a,node b)
{
if(fabs(a.val-b.val)>eps) return a.val<b.val;
return a.cnt<b.cnt;
}
node f[maxn][maxn];
node calc(double x)
{
for(int i=0;i<=n;i++) for(int j=0;j<=b;j++) f[i][j]={-inf,0};
f[0][0]={0,0};
for(int i=1;i<=n;i++)
for(int j=0;j<=b;j++)
{
f[i][j]=max(f[i-1][j],f[i-1][j]+(node){p[i]-x,1});
if(j) f[i][j]=max({f[i][j],f[i-1][j-1]+(node){q[i],0},f[i-1][j-1]+(node){p[i]+q[i]-p[i]*q[i]-x,1}});
}
return f[n][b];
}
int main()
{
scanf("%d%d%d",&n,&a,&b);
for(int i=1;i<=n;i++) scanf("%lf",&p[i]);
for(int i=1;i<=n;i++) scanf("%lf",&q[i]);
double l=0,r=1;
while(r-l>eps)
{
double mid=(l+r)/2;
node cur=calc(mid);
if(cur.cnt>=a) l=mid,res=cur.val+a*mid;
else r=mid;
}
printf("%.10lf\n",res);
return 0;
}
例5、\(\texttt{CF802O April Fools' Problem (hard)}\)
题目描述
有 \(n\) 天时间,第 \(i\) 天你可以花费 \(a_i\) 准备一道题、花费 \(b_i\) 打印一道题(一天内可以同时做这两件事),准备好的题可以留到以后打印。
你需要准备并打印 \(k\) 道题,求最小代价。
数据范围
- \(1\le k\le n\le 5\cdot 10^5\)。
时间限制 \(\texttt{10s}\) ,空间限制 \(\texttt{256MB}\) 。
分析
有一个显然的费用流建图,可以通过 medium 版本:
记源点为 \(s\) ,汇点为 \(t\) ,连边 \((s,s',k,0),(s',i,1,a_i),(i,t,1,b_i),(i,i+1,\infty,0)\) 。
由于费用关于流量为凸函数,考虑 \(\texttt{WQS}\) 二分。
准备一道题的代价为 \(a+b-x\) ,用小根堆维护已经插入的 \(a_i-x\) 的值。
有一个很 \(\text{naive}\) 的贪心想法,如果 \(b_i\) 加上堆顶小于零,那么我们选择这道题可以让总代价变小。
但是后面有可能出现更小的 \(b\) ,考虑增加一个反悔操作,将所有已经选择的 \(b\) 也加入堆中。
如果仅需要记录最小代价那么已经做完了,但还需要求最大题数。
对于每种决策,同时维护选择它能带来的题数增量。具体的,给 \(a_i-x\) 赋权值 \(1\) , \(b_i\) 赋权值 \(0\) 。
根据 pair
双关键字比较的特性,小根堆中应该存储权值的相反数。
时间复杂度 \(\mathcal O(n\log n\log V)\) 。
#include<bits/stdc++.h>
#define int long long
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=5e5+5;
int k,n,res;
int a[maxn],b[maxn];
pii calc(int x)
{
int cnt=0,val=0;
priority_queue<pii,vector<pii>,greater<pii>> q;
for(int i=1;i<=n;i++)
{
q.push(mp(a[i]-x,-1));
if(b[i]+q.top().fi<=0)
{
val+=b[i]+q.top().fi,cnt-=q.top().se,q.pop();
q.push(mp(-b[i],0));
}
}
return mp(cnt,val);
}
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++) scanf("%lld",&b[i]);
int l=0,r=2e9;
while(r-l>1)
{
int mid=(l+r)/2;
pii cur=calc(mid);
if(cur.fi>=k) r=mid,res=cur.se+mid*k;
else l=mid;
}
printf("%lld\n",res);
return 0;
}
例6、\(\texttt{P4983 忘情}\)
题目描述
定义一个序列的权值为 \((\sum x_i+1)^2\) 。
给定长为 \(n\) 的序列 \(x\) ,要求将其划分为 \(m\) 段,求每段权值之和的最小值。
数据范围
- \(1\le m\le n\le 10^5,1\le x_i\le 10^3\) 。
时间限制 \(\texttt{1s}\) ,空间限制 \(\texttt{500MB}\) 。
分析
记 \(s_j=\sum_{k=1}^jx_k\) , \(f_{i,j}\) 表示将 \([1,j]\) 划分为 \(i\) 段的最小代价,转移方程为:
可以感性理解一下为什么 \(f_{i,n}\) 是关于 \(i\) 的下凸函数:
用 \(f_i\) 代指上面的 \(f_{i,n}\) ,记 \(\Delta_i=f_{i-1}-f_i\) 。
\(f_i\) 可以看成在 \(f_{i-1}\) 的基础上切了一刀,假设切开的两段长度分别为 \(x,y\) ,则 \(\Delta_i=(x+y+1)^2-(x+1)^2-(y+1)^2=2xy-1\) 。
显然我们选择的 \(2xy-1\) 只会越来越小(否则与 \(f_i\) 的最优性矛盾),因此 \(f_i\) 为下凸函数。
注意这个证明并不严谨,因为 \(f_i\) 的方案并不一定能通过 \(f_{i-1}\) 切一刀得到。
考虑 \(\texttt{WQS}\) 二分,假设二分到斜率为\(x\),转移方程为:
对上述方程恒等变形:
可以看成用斜率为 \(s_j\) 的直线切平面上的点 \((2(s_k-1),f_k+(s_k-1)^2)\) 能得到的最小截距,斜率优化可以做到线性。
时间复杂度 \(\mathcal O(n\log V)\) 。
注意二分范围要覆盖所有可能的斜率,本题下界为 \(10^{-16}\) ,上界为 \(0\) 。
按照 \(\texttt{WQS}\) 二分的套路,转移代价相同时,我们要取段数最多的一个决策点,这意味着斜率相等时我们需要根据段数决策是否弹出队列。然而,本题不特判这个细节(判断是否弹队列时带等号)也能通过,而且笔者确实没能拍出 \(hack\) 数据。
#include<bits/stdc++.h>
#define int long long
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
#define x(k) (2*(s[k]-1))
#define y(k) (f[k]+(s[k]-1)*(s[k]-1))
using namespace std;
const int maxn=1e5+5;
int h,m,n,t,res;
int f[maxn],g[maxn],q[maxn],s[maxn];
long double slope(int i,int j)
{
return 1.0L*(y(j)-y(i))/(x(j)-x(i));
}
pii calc(int X)
{
q[h=t=1]=0;
for(int i=1;i<=n;i++)
{
while(h<t&&mp(slope(q[h],q[h+1]),g[q[h]])<mp(1.0L*s[i],g[q[h+1]])) h++;
f[i]=f[q[h]]+(s[i]-s[q[h]]+1)*(s[i]-s[q[h]]+1)-X,g[i]=g[q[h]]+1;
while(h<t&&mp(slope(q[t-1],q[t]),g[i])>mp(slope(q[t],i),g[q[t]])) t--;
q[++t]=i;
}
return mp(g[n],f[n]);
}
signed main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++) scanf("%lld",&s[i]),s[i]+=s[i-1];
int l=-1e16,r=0;
while(r-l>1)
{
int mid=(l+r)>>1;
pii cur=calc(mid);
if(cur.fi>=m) r=mid,res=cur.se+m*mid;
else l=mid;
}
printf("%lld\n",res);
return 0;
}
例7、\(\texttt{P4383 [八省联考 2018] 林克卡特树}\)
题目描述
给定一棵 \(n\) 个点的树,边权 \(w_i\) 可能为负。
你需要在树上删去恰好 \(k\) 条边,然后加上 \(k\) 条边权为 \(0\) 的边,求新树直径的最大可能值。
注:直径可以仅包含一个点,此时直径长度为零。
数据范围
- \(1\le n\le 3\cdot 10^5,0\le k\lt n,0\le|w_i|\le 10^6\) 。
时间限制 \(\texttt{10s}\) ,空间限制 \(\texttt{1000MB}\) 。
分析
容易发现加边操作是假的,删边后会形成 \(k+1\) 个连通块,我们只会把这些连通块的直径串起来。
注意到直径的定义就是最长链,因此题意可以转化为选 \(k+1\) 条不相交的链,求边权和最大值。
考虑树形 \(\texttt{dp}\) 。
\(f_{u,i,0/1/2}\)表示 \(u\) 子树中选了 \(i\) 条完整的链, \(deg_u=0/1/2\) 时的最大边权和。
转移 \((u,v,w)\) 这条边时需要作出的决策:
- \(deg_v\le 1\) ,在 \(v\) 子树内终结这条链。
- \(\max(deg_u,deg_v)\le 1\) ,将 \(v\) 所在链延伸到 \(u\) ,收益为 \(w\) 。
注:孤立点也可以当作一个连通块,因此需要赋初值 \(f_{u,1,2}=0\) 。
时间复杂度 \(\mathcal O(nk)\) ,可以获得 \(60\) 分。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=3e5+5,inf=1e9;
int k,n,u,v,w;
int sz[maxn],f[maxn][105][3];
vector<pii> g[maxn];
inline void chmax(int &x,int y)
{
if(x<=y) x=y;
}
void dfs(int u,int fa)
{
sz[u]=1,f[u][0][0]=0,f[u][1][2]=0;
for(auto p:g[u])
{
int v=p.fi,w=p.se;
if(v==fa) continue;
dfs(v,u);
static int tmp[105][3];
memset(tmp,0xcf,sizeof(tmp));
for(int i=0;i<=sz[u];i++)
for(int j=0;j<=sz[v];j++)
for(int x=0;x<=2;x++)
for(int y=0;y<=2;y++)
{
int a=y==1,b=x==1,val=f[u][i][x]+f[v][j][y];
if(i+j+a<=k) chmax(tmp[i+j+a][x],val);
if(i+j+b<=k&&x!=2&&y!=2) chmax(tmp[i+j+b][x+1],val+w);
}
sz[u]=min(sz[u]+sz[v],k);
memcpy(f[u],tmp,sizeof(tmp));
}
}
int main()
{
scanf("%d%d",&n,&k),k++;
for(int i=1;i<=n-1;i++)
{
scanf("%d%d%d",&u,&v,&w);
g[u].push_back(mp(v,w)),g[v].push_back(mp(u,w));
}
memset(f,0xcf,sizeof(f));
dfs(1,0);
printf("%d\n",max({f[1][k][0],f[1][k-1][1],f[1][k][2]}));
return 0;
}
凸性的证明还是老套路,能优先选的一定会优先选,因此差分数组不增。
二分斜率 \(mid\) ,每选择一条链会产生 \(mid\) 的代价。
这样上面\(\text{dp}\)数组的第二维限制消失了,把转移代价放进去即可。
注意斜率绝对值最大为原树直径,可以达到 \(3\cdot10^{11}\) 。
时间复杂度 \(\mathcal O(n\log V)\) 。
#include<bits/stdc++.h>
#define int long long
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=3e5+5,inf=1e18;
int k,n,u,v,w,res;
pii emp,val,f[maxn][3];
vector<pii> g[maxn];
inline pii operator+(pii a,pii b)
{
return mp(a.fi+b.fi,a.se+b.se);
}
inline void chmax(pii &a,pii b)
{
a=max(a,b);
}
void dfs(int u,int fa)
{
f[u][0]=mp(0,0),f[u][1]=mp(-inf,0),f[u][2]=val;
for(auto p:g[u])
{
int v=p.fi,w=p.se;
if(v==fa) continue;
dfs(v,u);
static pii tmp[3];
for(int i=0;i<=2;i++) tmp[i]=mp(-inf,0);
for(int i=0;i<=2;i++)
for(int j=0;j<=2;j++)
{
chmax(tmp[i],f[u][i]+f[v][j]+(j==1?val:emp));
if(i!=2&&j!=2) chmax(tmp[i+1],f[u][i]+f[v][j]+mp(w,0)+(i==1?val:emp));
}
for(int i=0;i<=2;i++) f[u][i]=tmp[i];
}
}
pii calc(int x)
{
val=mp(-x,1),dfs(1,0);
return max({f[1][0],f[1][1]+val,f[1][2]});
}
signed main()
{
scanf("%lld%lld",&n,&k),k++;
for(int i=1;i<=n-1;i++)
{
scanf("%lld%lld%lld",&u,&v,&w);
g[u].push_back(mp(v,w)),g[v].push_back(mp(u,w));
}
int l=-3e11,r=3e11;
while(r-l>1)
{
int mid=(l+r)>>1;
pii cur=calc(mid);
if(cur.se>=k) l=mid,res=cur.fi+k*mid;
else r=mid;
}
printf("%lld\n",res);
return 0;
}
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/17372864.html