[JZOJ3987]Tree

本文介绍了一种结合分数规划与动态规划解决特定类型树形结构问题的方法。通过二分查找与动态规划技巧,实现了对给定点权无根树的有效路径选择,以最大化得分。文章详细探讨了算法思路与实现细节,并通过实例代码展示了如何优化时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目大意

给定一棵有n个节点的点带权(vi)无根树。你需要选择一些互不相交(包括端点)的路径。如果你选择了k条路径,且覆盖的点权和为S,得分就为Sk+1
另外,在选取路径之前,你必须执行一次如下的操作(操作分为三个步骤):

  1. 选定整数参数C,满足C[0,T]
  2. 将所有点权加上C
  3. 将所有点权对LIMIT取模。

其中TLIMIT的值已经被给出,你需要求出可能的最大得分。

1n5×103,vi,t<LIMIT105


题目分析

先不考虑必须要执行的操作,假设我知道每一个点的权值,怎么做呢?
分数规划的常见套路:考虑二分答案ans,那么当前答案可行当且仅当存在方案满足:

Sk+1>ans


Sk×ans>ans]

那么怎么判断当前答案是否可行呢?可以发现,如果我们在计算时将每一条路径各自带来的得分都减去ans,那么左边式子是可以看成与路径条数k无关的。
因此我们使用动态规划算法来判定答案是否可行。我们随便选一个根节点,设figi分别表示节点i向上延伸/不向上延伸(包括它作为两个儿子向上延伸的交点和不选它两种情况),子树i的最大得分。
这个瞎讨论一下就可以转移,时间复杂度是O(n)的。也就是说我们可以在线性时间内完成对答案可行性的判定。
那么现在要考虑执行操作,显然我们没有必要对于[0,T]内的每一个数都考察一遍,因为如果有很多个C,它们都不会造成任何一个viLIMIT模掉,那么显然这些C中最大那个会造成更优的结果。因此我们只用考察LIMITvi1这种数(当然还要判断是否小于等于T之类的)。令E表示二分答案次数,现在时间复杂度是O(n2E)
这让还不能得到所有分数,怎么办呢?
在这题,我们可以线性判断对于一个点权序列,一个答案是否可行,但是要具体求出对于这个点权序列的最优解,就要算上二分的复杂度。
那么我们考虑加上一个优化,每次二分之前,先判定对于当前要考察的点权序列,目前的最优解是否可行,如果不可行,那么在这个点权序列我们不可能计算出更加优的结果,就不用在这里二分了。
但是这样我依然有可能每次都要二分,毕竟有可能很不好运,每一个考察值的答案都比前一个优。那怎么办?我们随机化!
将所有考察值去重,然后随机打乱,使用上面的方法判断。这样期望只会有logn个值要进行二分。这个证明需要用到一下结论:
  • 一个n个数随机顺序的排列,我们从第一个数开始,每次跳到后面第一个比自己大的位置,期望情况下需要logn步。
  • 证明:根据期望的线性性,我们只要计算出每一个位置对步数造成贡献的概率之和。显然一个位置i对答案造成贡献当且仅当在它前面没有更大的数,这个概率为1i,我们累加起来就是调和级数,是O(logn)的。

于是这里的时间复杂度变为O(n2+nElogn)了。


代码实现

请注意代码常数……

#include <algorithm>
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <ctime>

using namespace std;

typedef double db;

const db EPS=1e-8;
const db INF=1e+40;
const int N=5005;
const int E=N<<1;

int c[N],v[N],last[N];
int nxt[E],tov[E];
int n,T,LIM,tot,m;
db f[N],g[N];
db ans,sum;

void insert(int x,int y){tov[++tot]=y,nxt[tot]=last[x],last[x]=tot;}

void dfs(int x,int fa,db cur)
{
    f[x]=-cur,g[x]=0.0;
    db d1=-INF,d2=-INF,gsum=0.0;
    for (int i=last[x],y;i;i=nxt[i])
        if ((y=tov[i])!=fa)
        {
            dfs(y,x,cur);
            gsum+=g[y];
            if (d1<f[y]-g[y]) d2=d1,d1=f[y]-g[y];
            else if (d2<f[y]-g[y]) d2=f[y]-g[y];
        }
    f[x]=max(f[x],d1)+gsum+v[x];
    g[x]=max(g[x],max(d1+d2+cur+v[x],d1+v[x]))+gsum;
}

bool judge(db cur)
{
    dfs(1,0,cur);
    return max(f[1],g[1])>=cur;
}

db calc(db l,db r)
{
    db mid,ret=l;
    for (;r-l>=EPS;)
    {
        mid=(l+r)/2.0;
        if (judge(mid)) l=(ret=mid)+EPS;
        else r=mid-EPS;
    }
    return ret;
}

void solve()
{
    ans=0.0;
    for (int i=1;i<=m;i++)
    {
        for (int j=1;j<=n;j++) v[j]+=c[i],v[j]>=LIM?v[j]-=LIM:0;
        if (judge(ans)) ans=calc(ans,sum);
        for (int j=1;j<=n;j++) v[j]-=c[i],v[j]<0?v[j]+=LIM:0;
    }
}

int main()
{
    srand(time(0));
    freopen("tree.in","r",stdin),freopen("tree.out","w",stdout);
    scanf("%d%d",&n,&LIM);
    for (int i=1;i<=n;i++) scanf("%d",&v[i]),sum+=v[i]+LIM;
    for (int i=1,x,y;i<n;i++) scanf("%d%d",&x,&y),insert(x,y),insert(y,x);
    scanf("%d",&T);
    for (int i=1;i<=n;i++) c[i]=min(T,LIM-1-v[i]);
    sort(c+1,c+1+n);
    m=0;
    for (int i=1;i<=n;i++) if (i==1||c[i]!=c[m]) c[++m]=c[i];
    random_shuffle(c+1,c+1+m);
    solve();
    printf("%.6lf\n",ans);
    fclose(stdin),fclose(stdout);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值