P2495 [SDOI2011]消耗战 (level 3)(虚树)

这是一篇关于如何利用虚树解决SDOI2011的消耗战问题的博客。文章介绍了虚树的特点和应用,特别是在处理询问点总和不超过500000的情况下的高效性。通过解析题目,作者解释了如何通过树形DP和路径压缩来找到最小权值的删除方案,以确保从根节点1无法到达指定的询问点。文章还对比了两位大佬不同的实现方式,并提供了自己的代码实现。
摘要由CSDN通过智能技术生成

题目链接

题意:

给你一棵n个点的树,树上的边有权值>0

然后给你m个询问,每一次询问给你k个点(不包含1(根节点))

让你在树上删除几条边,使得从1出发,无法到达这k个点,并使得删掉的边的权值尽可能小,输出权值

Σki<=500000

解析:

这道题我是先看了虚树,然后再做这道题。

用虚树做的题目有一个很明显的特征就是询问的点的总和(也就是上面的Σki<=500000)是在1e8以内的。

因为虚树的复杂度就是O(询问的点的总和)=O(Σki)=O(500000)

大佬讲解1

大佬讲解2

虚树总结一下就是每一次询问,对整棵树进行路径压缩,将没有用的点并掉,

使得只剩下本次询问的点以及本次询问的点之间的lca

。如果本次询问的点是k,那么虚树上最多的点只会是2*k。那么我们对他进行树形dp的时候,在线dp的复杂度就降下来了。

然后这里很关键的一步就是对路径的压缩,怎么压缩是取决于你到底要dp什么东西来决定的。

以这道题举例,我们需要求的是在一棵虚树上,删掉哪些边使得,根节点1与询问点不连通。

如果按正着来的话,每一个点u应该记录的是以u为根的子树中,删除哪些边使得询问的点与u不连通。

那么dp[u]=min(dp[v],mp[u][v]) v∈son(u)

但是这样dp的话,路径压缩过后,我们需要维护的是虚树中父节点与孩子节点的边的路径上的权值最小的边

譬如u在虚树中,v不在,但v的孩子vv在,那么我们就需要计算mp[u][vv]的权值=min(mp[u][v],mp[v][vv])

这样维护就增加了操作。所以对于这道题的dp最好是倒着来,因为每一个点向下对应一棵树,但是向上就只

对应一条链。那么我们就向上维护每一个点到根节点1,路径上的最小值mn[u]。这样压缩过后,因为根节点还是

不变的,所以不需要额外的处理。那么对虚树dp的时候转移方程就是

(现在dp[u]表示删除某些边,使得u的子树内的点不与根节点1连通)

1.如果u是询问点,那么dp[u]=mn[u]

2.否则dp[u]=min(mn[u],Σdp[v]) v∈son(u)

 

然后虚树还有一点是,我看不同的人写的代码,最后建出来的虚树也是不一样的。

上面两个大佬的代码就不一样,不过其实就是一种情况的不同

他们建出来的树会有一点不一样

询问 4 5 7 8 2点

大佬1:

void insert(int u){
    if(top <= 1) {stk[++top] = u;return ;}
    int lca = LCA(u,stk[top]);
    if(lca == stk[top]) {stk[++top] = u;return ;}  //若dfn[y]=dfn[lca],建边
    while(top > 1 && dfn[lca] <= stk[top-1]) {
        addedge(stk[top-1],stk[top]);
        --top;
    }
    if(lca != stk[top]) stk[++top] = lca;
    stk[++top] = u;
}

大佬2:

void insert(int x) {
    if(top == 1) {s[++top] = x; return ;}
    int lca = LCA(x, s[top]);
    if(lca == s[top]) return ; //若dfn[y]=dfn[lca],即y=lca,连边lca−>x,此时子树构建完毕(break);不建边
    while(top > 1 && dfn[s[top - 1]] >= dfn[lca]) add_edge(s[top - 1], s[top]), top--;
    if(lca != s[top]) add_edge(lca, s[top]), s[top] = lca;  //若dfn[y]<dfn[lca],即lca在y,x之间,连边lca−>x,x出栈,再将lca入栈
    s[++top] = x;
}

 大佬2的代码其实就是把询问的点都变成了叶子节点,因为2也是需要断开的,那么无论4 5 7 8怎么断,最后还是要断1 -2使得

1与2不连通,所以2下面的询问点都是不需要了。每次插入点的时候栈顶一定是询问点!

最后放一个我的代码

// luogu-judger-enable-o2
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <map>
using namespace std;
const int N = 250000+10;
typedef long long ll;
const ll INF = 0x3f3f3f3f3f3f3f3f;

typedef struct node
{
    int v;
    ll w;
    node(int vv=0,ll ww=0):v(vv),w(ww){}
}node;

int pos[N],Log[N<<2],ST[N<<2][25]; //pos[i]:i第一次出现的位置,ST[i][j]在欧拉序[i,i+(1<<j))中dep最小的点
int tot;
//int fa[N][25]; //fa[i][j]记录第i个节点的第(1<<j)个父亲,(非必要)
int dep[N];
vector<node> ee[N];
vector<int> edge[N];
int dfn[N],cnt;
ll mn[N];

void add1(int u,int v,ll w)
{
    ee[u].push_back(node(v,w));
}

void add2(int u,int v)
{
    edge[u].push_back(v);
}




int Min(int x,int y) { 
    return dep[x] < dep[y] ? x : y;
}


void dfs(int u,int fc)
{
    ST[++tot][0] = u; pos[u] = tot;
    dfn[u]=cnt++;
    for (int i = 0; i<ee[u].size(); i ++) {
        node tmp=ee[u][i];
        int v = tmp.v;
        if (v == fc) continue;
        dep[v] = dep[u] + 1;
        mn[v]=min(mn[u],tmp.w);  //用于树形dp,路径压缩
        dfs(v,u);
        ST[++tot][0] = u;//!
    }

}

void init(int n)
{
    tot=0;
    dfs(1,0);
    Log[0] = -1;
    for (int i = 1; i <= tot; ++i) Log[i] = Log[i >> 1] + 1;
    //for (int j = 1; j <= Log[n]; ++j) 
    //	for (int i = 1; i <= n; ++i) fa[i][j] = fa[fa[i][j - 1]][j - 1];
    for (int j = 1; j <= Log[tot]; ++j) 
        for (int i = 1; i <= tot; ++i) ST[i][j] = Min(ST[i][j - 1], ST[i + (1 << (j - 1))][j - 1]);

}

int LCA(int u,int v) {
    if (u == v) return u;
    u = pos[u], v = pos[v];
    if (u > v) swap(u, v); 
    //u ++;   //?
    int k = Log[v - u + 1];
    return Min(ST[u][k], ST[v - (1 << k) + 1][k]);
}

int stk[N],top;
int que[N];
int mark[N];

bool cmp(int a,int b)
{
    return dfn[a]<dfn[b];
}

void insert(int u){  //虚树插入
    if(top == 1) {stk[++top] = u;return;}
    int lca = LCA(u,stk[top]);
    if(lca == stk[top]) {stk[++top] = u;return ;}
    while(top > 1 && dfn[lca] <= dfn[stk[top-1]]){
        add2(stk[top-1],stk[top]);
        --top;
    }
    if(lca != stk[top]) {
        add2(lca,stk[top]);
        stk[top] = lca;
    } 
    stk[++top] = u;
}

ll DP(int u)
{
    if(edge[u].empty())
    {
        mark[u]=0;  //!
        return mn[u];
    }
    ll sum=0;
    for(int v:edge[u])
    {
        sum+=DP(v);
    }
    ll ans=mn[u];
    if(!mark[u]) ans=min(ans,sum);  //放下面是为了下面的删边初始化,不然不好处理
    edge[u].clear();
    mark[u]=0;
    return ans;
}



int main()
{
    int n;
    scanf("%d",&n);
    mn[1]=INF;
    cnt=1;
    for(int i=1;i<n;i++)
    {
        int u,v;
        ll w;
        scanf("%d%d%lld",&u,&v,&w);
        add1(u,v,w);
        add1(v,u,w);
    }
    init(n);
    int m;
    scanf("%d",&m);
    for(int i=1;i<=m;i++)
    {
        int k;
        scanf("%d",&k);
        top=0;
        for(int j=0;j<k;j++) 
            scanf("%d",&que[j]),mark[que[j]]=1;
        sort(que,que+k,cmp);
        stk[++top]=1;
        for(int j=0;j<k;j++) insert(que[j]);
        while(top>1)
            add2(stk[top-1],stk[top]),top--;
        
        ll ans=DP(1);
        printf("%lld\n",ans);
        
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值