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}\gets f_{i-1,j,k}\\ f_{i,j+1,k}\gets f_{i-1,j,k}+p_i\\ f_{i,j,k+1}\gets f_{i-1,j,k}+q_i\\ f_{i,j+1,k+1}\gets f_{i-1,j,k}+p_i+q_i-p_i\cdot q_i\\ \]

大胆猜测 \(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,j}=\min_{0\le k\lt j}\big(f_{i-1,k}+(s_j-s_k+1)^2\big)\\ \]

可以感性理解一下为什么 \(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\),转移方程为:

\[f_j=\min_{0\le k\lt j}\big(f_k+(s_j-s_k+1)^2-x\big)\\ \]

对上述方程恒等变形:

\[f_k+(s_k-1)^2=s_j\cdot 2(s_k-1)+f_j-s_j^2+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;
}
posted @ 2023-05-04 23:14  peiwenjun  阅读(0)  评论(0编辑  收藏  举报