[Luogu4383] [九省联考2018] 林克卡特树 [带权二分][树形dp]

[ L i n k \frak{Link} Link]


题目大意:
给一颗N点的树。现在在里面切掉k条边并且连接k条0边、求方案最大化权值和最大的路径。
1 <= N <= 3 * 105, 0 <= k <= 3 * 105, |vi| <= 1 * 106


10pts
动态加&删边 lct!(实际上只有一次询问,考虑应该是树形dp。
首先很明显可以看出来一个结论就是加的边必须连接两个不同的连通块。这个结论先放着。
发现权值和最大路径也就是(带权)直径,所以10pts可以做了
考虑一下20pts怎么做。


20pts
看起来比较熟悉。
现在是删掉一条带权的边然后加上一条零边;
我们可以考虑一个连通块里面的最长+次长路径?然后暴力枚边拆开然后连边?
我们只要考虑拆成两个连通块,然后用简单的树形dp处理。


35pts
emmm 删两条加两条?当然不能拿20pts的做法直接套;于是就有点尴尬
不要考虑具体的数字,我们可以大概往60那边想?如果k=100?
现在k变大了又有一种熟悉感,就是子树里面删多少边连多少边的最大权一类
这个分摊一下树形分组背包就可以了,计算最大&次大的时候带一下
然后这样的复杂度是 Θ ( n k 2 ) \Theta(nk^2) Θ(nk2),所以60无望(


60pts
那就是要做到 Θ ( n k ) \Theta(nk) Θ(nk)。考虑一下还有什么其他的性质。
不考虑Δ,改边后边权为0的边显然就是对答案没有贡献了;
诶那能不能直接当作没有啊
没有了会怎么样?那我们就得选不多于k+1条不相交的链;
实际上因为要限制cut了k条边,实际上得限制选正好k+1条不相交的链——
允许把点也看作链就可以。看起来像是经典tree dp。
这道题给了二叉树的条件,所以方便转移可以直接分类讨论。(如果不限制二叉也会炸)
要知道给每个儿子分了多少条链,就加一个状态表示当前点在操作后还剩几个儿子。0/1/2。


100pts
到这里更优的做法本质上都是60pts那种了,因为性质已经差不多被利用完了。
朴素的dp显然复杂度只能是n2;看范围感觉上比较像是带log的,
这种多次修改最后来一个查询的也很少有 Θ ( n + k ) \Theta(n+k) Θ(n+k)
暗示dp优化。考虑一下斜率啊单调啊那些东西。

首先放弃思考打一个表观察一下发现上凸,然后想想粗略证明?
选正好m个物品,求最大值;
如果是不多于m个物品那优化方法就多了,现在限制刚好m个。这个形式比较像斜率凸优化。
考虑一哈斜率凸优化的经典套路:打表发现dp[i][j]的最大值关于j的斜率单调不递增
是一个上凸函数。

同时简单考虑也可以发现切的边越多、新切的边的贡献就越小。(最优意义下)
这个有什么用?
考虑到我们要求凸包上面纵坐标最大的点;
好像是个线性规划。
好像就可以拿一条直线去逼近它。
那好像就可以二分啦。每次康康直线切到哪。
至于具体怎么做自己想(逃
Θ ( N l o g ∑ w i ) \Theta(Nlog\sum w_i) Θ(Nlogwi)

我们知道二分斜率;可是具体要怎么操作?
斜率放到题目里面,直线方程y=kx+b,
有b=y-kx,切点要求b最大。我们可以把斜率当作选一条链的代价——
然后我们现在每次二分k,要树形dp求max(y-kx)也就是max(f(x)-kx)
令dp[x]=max{f(x)-kx}。然后目标就变得清晰起来了。
我们求出最优解——也是当前斜率切凸包的切点。
二分的时候怎么更新边界?我们只需要判断当前斜率切到的点x有没有大于k就可以了。
(实际上切到的应该是一条线,取我们需要的那个(x小的)端点就可以啦。这个x一定是整数。)
实际上我们要求的切点x坐标就是选的链数,记录一下就可以了。
为了方便,我们选择在链封闭的时候累计。

注意虽然我上面说了好多个k,但是实际上要选的链数是k+1条。

从费用流入手思考


#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<cstring>
#include<cmath>
#include<ctime>
#include<cctype>
#include<algorithm>
using namespace std;
int n, k, tot;
int head[300005];
int nxt[600005];
int to[600005];
long long val[600005];
long long Mid;
class Node {
    
    private:
        
    public:
    
        long long lValue;
        int nCount;
        Node(long long lArg = 0, int nArg = 0) {
            lValue = lArg;
            nCount = nArg;
        }
        bool operator < (const Node& sTar) const {
            return (lValue == sTar.lValue) ? nCount > sTar.nCount : lValue < sTar.lValue;
        }
        Node operator + (const Node& sTar) const {
            return Node(lValue + sTar.lValue, nCount + sTar.nCount);
        }
        Node operator - (const Node& sTar) const {
            return Node(lValue - sTar.lValue, nCount - sTar.nCount);
        }
    
};
Node F[300005][3];
const long long inf = -2147483647147483647;
#define add_edge(a, b, c) nxt[++tot] = head[a], head[a] = tot, to[tot] = b, val[tot] = c
int dp(const int &x, const int &fa) {
    F[x][0] = F[x][1] = Node(0, 0);
    F[x][2] = max(Node(0, 0), Node(-Mid, 1)); //有链退化为点的情况,把不选和自闭取个max 
    for (register int v, j = head[x]; j; j = nxt[j]) {
        v = to[j];
        if (v == fa) continue;
        dp(v, x);
        
        F[x][2] = max(F[x][2] + F[v][0], F[x][1] + F[v][1] + Node(val[j] - Mid, 1));
        //之前的子树选满两条链了 或 之前的选了一条,当前这个也选一条。把新链连上来,闭链
        
        F[x][1] = max(F[x][1] + F[v][0], F[x][0] + F[v][1] + Node(val[j], 0));
        //之前的子树选了一条,当前这个不选 或 之前的没有选,当前这个选一条接上来,不闭链
        
        F[x][0] = F[x][0] + F[v][0];
        //都不选
    }
    //在此之前,F[x][0~2]都不考虑向上继承,只考虑子树内的情况
    
    //在此之后,
    //F[x][0]就已经是准备好随时上传的状态
    //F[x][1]暂时放着不更新,之后再来
    //(要在这里更新也是可以的,但是麻烦+后面可能要闭链不如推迟一下)
    //F[x][2]已经在下面闭链了,无法对上面产生贡献。
    F[x][0] = max( max(F[x][0], F[x][1] + Node(-Mid, 1) ), F[x][2] );
    //顺便F[x][0]可以当作统计答案(
}
int main() {
    scanf("%d%d", &n, &k);
    ++k; //别忘了是选k+1条链。。。 
    long long sum = 0, w;
    for (register int u, v, i = 1; i < n; ++i) {
        scanf("%d%d%lld", &u, &v, &w);
        add_edge(u, v, w);
        add_edge(v, u, w);
        sum += (w >= 0) ? w : (-w);
    }
    long long L = -sum, R = sum;
    while (L <= R) {
        Mid = L + R >> 1;
        dp(1, 0);
        if (F[1][0].nCount <= k) {
            R = Mid - 1;
        } else {
            L = Mid + 1;
        }
    } //假如R=L+1,L时候的F[1][0].nCount<k,R时候的>k,我们应当选L
    //(也就是说、最后那个Mid一定不大于k)
    //为什么?因为我们在上面的dp里留了余地,还可以随便再多抓几个点来当成链。
    //所以<=k都可以,>k一定不行。
    if (Mid != L) {
    	Mid = L;
    	dp(1, 0);
    }
    printf("%lld", Mid * k + F[1][0].lValue); //把多花的代价搞回来
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值