潜龙未见静水流,沉默深藏待时秋。一朝破空声势振,惊世骇俗展雄猷。
随笔 - 80, 文章 - 0, 评论 - 2, 阅读 - 1977

WQS 二分学习笔记

目录

一、算法介绍
二、相关例题
例1、P2619 [国家集训队]Tree I
例2、P5633 最小度限制生成树
例3、CF125E MST Company 题解
例4、CF739E Gosha is hunting
例5、CF802O April Fools' Problem (hard)
例6、P4983 忘情
例7、P4383 [八省联考 2018] 林克卡特树

一、算法介绍

WQS 二分又称带权二分、凸优化,因王钦石神仙最先提出而得名。

WQS 二分能够解决的问题类型:

有若干个物品可供选择,某些特殊物品限制恰好m 个,求最优方案。

前提条件:

  • fi 为限制恰好选 i 个的答案,那么 (i,fi) 是凸函数。
  • 去掉限制后可以快速计算答案。

凸性是根据题目性质分析出来的,我们事先并不知道凸包的形状,同时目标变为计算 fm


WQS 二分干的事情是二分斜率,快速计算斜率为 k 的直线会切在凸包上的哪个点

注意到 (x,fx) 对应的截距为 fxkx ,可以看成**将每个特殊物品的代价都减去 k **。

以上凸壳为例,求切点等价于求 fxkx 的最大值。

而这等价于将每个特殊物品的代价减去 k 后,计算没有特殊限制的答案。

对于大多数题目, fx 为整数,那么斜率 Δx=fxfx1 也为整数,使用整数二分就足够了。

只有当 fx 为实数时,才需要使用实数二分。

二分边界应为斜率的最值,即 min,maxΔi


WQS 二分思路还算清晰,然而三点共线的细节非常搞心态。

换一种方式理解:目标找到使得切点横坐标 m 的最大(上凸)或最小(下凸)斜率 k

正确操作:记二分到的切点为 (x,fx) ,在闭区间的一侧用 fx+km (而不是 fx+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、P2619 [国家集训队]Tree I

题目描述

给定一张 n 个点, m 条边的带权无向图,每条边为黑色或白色。

求一棵权值和最小且恰有 k 条白色边的最小生成树。

数据范围

  • 1n5104,1m105,1wi100

时间限制 2s,空间限制500MB

分析

fi 为恰有i条白色边的最小生成树权值之和,不存在为

关于 fi 为下凸函数的证明:

f0 可以看成仅用黑边跑 kruskal 算法(不存在的边权值视为 ),那么 fi 就是在 fi1 的基础上,加入一条白边,再从环上删掉一条权值最大的黑边。

如果第 i+1 次替换的增量 Δi+1=fi+1fi 比第 i 次的 Δi=fifi1 小,那么对于 fi ,我们放弃第 i 次替换,执行第 i+1 次替换,容易发现这也是一棵合法的生成树。

fi=fiΔi+Δi+1<fi ,这与 fi 的最优性矛盾。

因此 ΔiΔi+1 ,即 fi 为下凸函数。

二分斜率 mid ,将所有白边的权值减去 mid 后跑 kruskal ,切点横坐标为白边个数。

注意为了保证最优位置横坐标最大,我们要尽可能多的使用白边,在排序时边权相同的白边在前。

时间复杂度 O((n+m)lognlogV)

可以将黑边和白边分别排序后做归并,时间复杂度降至 O((n+m)(logn+logV)) ,但是我懒。

#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、P5633 最小度限制生成树

题目描述

给定一张 n 个点, m 条边的带权无向图,要求 s 号点恰好连了 k 条边,求权值和最小的生成树,无解输出 Impossible

数据范围

  • 1n5104,1m5105,1k100,0w3104

时间限制 8s ,空间限制 256MB

分析

先判掉无解的情况:

  • 原图不连通。
  • 给边去重后s 的出边数量 <k
  • 加入所有与 s 无关的边后,连通块(不包含 {s} )数量 >k

fk 为限制 s 恰好连接 k 条边的最小权值,不存在为

关于 fk 为下凸函数的证明:

fk 可以看成在 fk1 的基础上,加入一条从 s 出发的边,再从环上删掉一条与 s 无关的边。

Δk=fkfk1 ,如果 Δk>Δk+1 ,我们放弃第 k 次替换,转而执行第 k+1 次替换,容易发现这仍然是一棵合法的 degs=k 的生成树,但与 fk 的最小性矛盾。

因此 ΔkΔk+1 ,即 fk 为下凸函数。

接下来套一个 WQS 二分板子就可以了。

本题数据范围较大,用归并排序实现,时间复杂度 O((n+m)(logn+logV))

#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、CF125E MST Company 题解

参考题解

题目描述

给定一张 n 个点, m 条边的带权无向图,保证图中没有重边、自环。

求一棵满足 deg1=k 的边权和最小的生成树,输出方案,无解输出 -1

数据范围

  • 1n5103,0m105,0k5000,1wi105

时间限制 8s ,空间限制 256MB

分析

容易发现本题其实比上一题就多了一个输出方案。

记从 1 出发的边为白边,其余为黑边。

网上常见的错解:将白边边权减去 r 后跑 kruskal ,跑到 k 条白边后忽略后面的所有黑边。

原因在于 WQS 二分仅能保证存在一种恰有 k 条白边的方案,但上述构造却指定了这 k 条白边,因此很容易让答案偏大或返回无解。

然而原题数据太水全放过了,下面是正确的构造方法:

先用最少白边的策略跑 kruskal固定所有白边,考虑在它的基础上构造方案。

再用最多白边的策略跑 kruskal ,记 limw尚未固定的边权大于 w 的白边数量最小值

保留所有白边,对于第 i 层(边权为 i 的所有边),优先使用白边,直到满足 now+limi=k ,层内其他边再使用黑边。

这个做法的正确性在于,根据 kruskal 算法的性质,每一层连完以后连通块形状固定,而我们已经考虑了后面所有层的限制。

时间复杂度 O((n+m)(logn+logV))

拍了几万组数据,应该没假。

#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、CF739E Gosha is hunting

题目描述

n 个物品, aA 类球和 bB 类球, A 类球和 B 类球抓到第 i 个物品的概率分别为 pi,qi

每类球最多在同一个物品上使用一次,但一个物品可以同时使用两类球。求期望抓到物品个数的最大值,要求绝对或相对误差 104

数据范围

  • 2n2103,0a,bn,0pi,qi1

时间限制 5s ,空间限制 256MB

分析

容易想到一个 O(n3)dpfi,j,k 表示考虑前 i 个物品,使用 jA 类球和 kB 类球的最大收益。

转移方程如下:

fi,j,kfi1,j,kfi,j+1,kfi1,j,k+pifi,j,k+1fi1,j,k+qifi,j+1,k+1fi1,j,k+pi+qipiqi

大胆猜测 fi,j,k 关于 j 是上凸函数。

WQS 二分优化,相当于 A 类球数量不限,但每使用一次会产生 mid 的代价。

于是 dp 降维后复杂度变为 O(n2)

注意需要实数二分,时间复杂度 O(n2logV)

本题凸性可以用费用流证明:

从源点向 A,B 类球分别连容量为 a,b 的边,从 A,B 类球向每个物品连容量为 1 ,费用为 pi,qi 的边,再从每个物品向汇点连边 (1,0),(1,piqi) ,跑最大费用最大流即可。

由于增广路长度非严格递减,因此代价关于流量是凸函数。

Warning:

  • 二分套二分是假做法,因为 WQS 二分需要记录使用的 A 球数量最大值,而二分套二分需要记录在 B 球数量恰好为 b 的前提下,使用 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、CF802O April Fools' Problem (hard)

题目描述

n 天时间,第 i 天你可以花费 ai 准备一道题、花费 bi 打印一道题(一天内可以同时做这两件事),准备好的题可以留到以后打印。

你需要准备并打印 k 道题,求最小代价。

数据范围

  • 1kn5105

时间限制 10s ,空间限制 256MB

分析

有一个显然的费用流建图,可以通过 medium 版本:

记源点为 s ,汇点为 t ,连边 (s,s,k,0),(s,i,1,ai),(i,t,1,bi),(i,i+1,,0)

由于费用关于流量为凸函数,考虑 WQS 二分。

准备一道题的代价为 a+bx ,用小根堆维护已经插入的 aix 的值。

有一个很 naive 的贪心想法,如果 bi 加上堆顶小于零,那么我们选择这道题可以让总代价变小。

但是后面有可能出现更小的 b ,考虑增加一个反悔操作,将所有已经选择的 b 也加入堆中。

如果仅需要记录最小代价那么已经做完了,但还需要求最大题数。

对于每种决策,同时维护选择它能带来的题数增量。具体的,给 aix 赋权值 1bi 赋权值 0

根据 pair 双关键字比较的特性,小根堆中应该存储权值的相反数。

时间复杂度 O(nlognlogV)

#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、P4983 忘情

题目描述

定义一个序列的权值为 (xi+1)2

给定长为 n 的序列 x ,要求将其划分为 m 段,求每段权值之和的最小值。

数据范围

  • 1mn105,1xi103

时间限制 1s ,空间限制 500MB

分析

sj=k=1jxkfi,j 表示将 [1,j] 划分为 i 段的最小代价,转移方程为:

fi,j=min0k<j(fi1,k+(sjsk+1)2)

可以感性理解一下为什么 fi,n 是关于 i 的下凸函数:

fi 代指上面的 fi,n ,记 Δi=fi1fi

fi 可以看成在 fi1 的基础上切了一刀,假设切开的两段长度分别为 x,y ,则 Δi=(x+y+1)2(x+1)2(y+1)2=2xy1

显然我们选择的 2xy1 只会越来越小(否则与 fi 的最优性矛盾),因此 fi 为下凸函数。

注意这个证明并不严谨,因为 fi 的方案并不一定能通过 fi1 切一刀得到。

考虑 WQS 二分,假设二分到斜率为x,转移方程为:

fj=min0k<j(fk+(sjsk+1)2x)

对上述方程恒等变形:

fk+(sk1)2=sj2(sk1)+fjsj2+x

可以看成用斜率为 sj 的直线切平面上的点 (2(sk1),fk+(sk1)2) 能得到的最小截距,斜率优化可以做到线性。

时间复杂度 O(nlogV)

注意二分范围要覆盖所有可能的斜率,本题下界为 1016 ,上界为 0

按照 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、P4383 [八省联考 2018] 林克卡特树

题目描述

给定一棵 n 个点的树,边权 wi 可能为负。

你需要在树上删去恰好 k 条边,然后加上 k 条边权为 0 的边,求新树直径的最大可能值。

注:直径可以仅包含一个点,此时直径长度为零。

数据范围

  • 1n3105,0k<n,0|wi|106

时间限制 10s ,空间限制 1000MB

分析

容易发现加边操作是假的,删边后会形成 k+1 个连通块,我们只会把这些连通块的直径串起来。

注意到直径的定义就是最长链,因此题意可以转化为k+1 条不相交的链,求边权和最大值。

考虑树形 dp

fu,i,0/1/2表示 u 子树中选了 i 条完整的链, degu=0/1/2 时的最大边权和。

转移 (u,v,w) 这条边时需要作出的决策:

  • degv1 ,在 v 子树内终结这条链。
  • max(degu,degv)1 ,将 v 所在链延伸到 u ,收益为 w

注:孤立点也可以当作一个连通块,因此需要赋初值 fu,1,2=0

时间复杂度 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 的代价。

这样上面dp数组的第二维限制消失了,把转移代价放进去即可。

注意斜率绝对值最大为原树直径,可以达到 31011

时间复杂度 O(nlogV)

#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 on   peiwenjun  阅读(4)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
历史上的今天:
2022-05-04 CF1437G Death DBMS 题解

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
点击右上角即可分享
微信分享提示