【XSY1905】【XSY2761】新访问计划 二分 树型DP

题目描述

  给你一棵树,你要从\(1\)号点出发,经过这棵树的每条边至少一次,最后回到\(1\)号点,经过一条边要花费\(w_i\)的时间。

  你还可以乘车,从一个点取另一个点,需要花费\(c\)的时间。

  你最多做\(k\)次车。

  问最短时间。

  \(k\leq n\leq 20000,w,c\leq 50000\)

题解

  我们考虑把最终路线中坐车的部分替换成走路。

  那么显然不会经过一条边超过两次。

  但是每条边都要经过者少一次,所以每条边只能被一个坐车的路线覆盖。

  所以我们要选择不超过\(k\)条不相交的链,把这些链用\(c\)的代价覆盖掉。

  可以用树上背包做。

  时间复杂度:\(O(nk)\)

  还有有一个网络流的做法:每次找树上最长链,然后用\(c\)的代价覆盖掉,即把路径上的边权取反。

  正解:

  如果我们可以调整乘车的代价,并把乘车次数设为无限次,那么当最优方案的乘车次数不超过\(k\)时最优方案的路线就是最优路线。

  这个东西可以用一次树型DP解决。

  设\(f_i\)为从\(i\)开始遍历以\(i\)为根的子树并回到\(i\)的最小代价和乘车次数,\(g_i\)为从\(i\)开始遍历以\(i\)为根的子树并乘车回到\(i\)的最小代价和乘车次数。

  然后随便DP一下就行了。

  可以观察到,乘车次数是随着乘车代价单调下降的(可能是非连续的),所以可以二分乘车代价,得到答案。

  时间复杂度:\(O(n\log nw)\)

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<utility>
using namespace std;
typedef unsigned un;
typedef pair<un,un> pii;
const int inf=0x3fffffff;
vector<pii> t[100010];
int n,k,c;
un cost;
pii f[100010];
pii g[100010];
pii operator +(pii a,pii b)
{
    return pii(a.first+b.first,a.second+b.second);
}
pii operator -(pii a,pii b)
{
    return pii(a.first-b.first,a.second-b.second);
}
void dp(int u,int fa)
{
    f[u]=pii(0,0);
    g[u]=pii(cost,1);
    for(auto a:t[u])
        if(a.first!=fa)
        {
            int v=a.first;
            int w=a.second;
            dp(v,u);
            pii f1=pii(inf,0);
            pii g1=pii(inf,0);

            f1=min(f1,f[u]+f[v]+pii(w,0));
            f1=min(f1,f[u]+f[v]+pii(cost,1));
            f1=min(f1,f[u]+g[v]);
            f1=min(f1,g[u]+f[v]);
            f1=min(f1,g[u]+g[v]-pii(cost,1));

            g1=min(g1,f[u]+f[v]+pii(cost,1));
            g1=min(g1,f[u]+g[v]);
            g1=min(g1,g[u]+f[v]+pii(w,0));
            g1=min(g1,g[u]+g[v]);

            f[u]=f1;
            g[u]=g1;
        }
}
void solve()
{
    for(int i=1;i<=n;i++)
        t[i].clear();
    int x,y,z;
    int sum=0;
    for(int i=1;i<n;i++)
    {
        scanf("%d%d%d",&x,&y,&z);
        x++;
        y++;
        t[x].push_back(pii(y,z));
        t[y].push_back(pii(x,z));
        sum+=z;
    }
    cost=c;
    dp(1,0);
    if(f[1].second<=k)
    {
        printf("%d\n",sum+f[1].first);
        return;
    }
    int l=0,r=inf;
    while(l<r)
    {
        cost=(l+r)>>1;
        dp(1,0);
        if(f[1].second>k)
            l=cost+1;
        else
            r=cost;
    }
    cost=l;
    dp(1,0);
    int ans=f[1].first-k*(l-c)+sum;
    printf("%d\n",ans);
}
int main()
{
#ifndef ONLINE_JUDGE
    freopen("b.in","r",stdin);
    freopen("b.out","w",stdout);
#endif
    while(~scanf("%d%d%d",&n,&k,&c))
        solve();
    return 0;
}

转载于:https://www.cnblogs.com/ywwyww/p/8622573.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值