最小行走距离(dfs+虚树)

好吧,感觉标题的名字取得有一点奇葩呢!源于我的高度概括

所以再做一些具体放阐述,这个问题大概意思是:在树上指定了一些点,你可以从树上的任意点出发,求走完所有指定点的最小行走距离(可存在一条边重复走,走几次这条边的权值就加几次)。

这个问题呢,我是在训练考试中碰到的,但是题目和要求做过一些小修改,为了更好的理解这种思想,我搜索到了这样一道最原始的题。

[SDOI2015]寻宝游戏

如何保证距离最短?我们可以做出这样的分析:

对于一个点,我们可以向子树走,也可以向父亲走。向父亲走肯定没有向子树走更优,因为你迟早要走一遍子树,先走父亲不过是又多加了一段重复走的路。(因为还要下来)

所以对于每一个点,我们选择先走子树再往上走->dfs序

而为什么说是虚树呢?其实我觉得不说虚树这个概念也没什么,因为我们只需要走那些被选择的点,而没有走完整棵树。

因为是动态的,我们考虑在当前状态下再加入一个点和删去一个点要怎么处理:

对于一条链,每一次变化时找到这个点插入的前驱pre和后继nxt,加入的时候删除dis(pre,nxt),加上dis(pre,x) + dis(x,pre),删除同理。

然后这个dfs序就用set维护

#include<bits/stdc++.h>
#define N 100003
#define LL long long
using namespace std;
int read()
{
    int x=0,f=1;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    return x*f;
}
struct EDGE{
    int to,nextt,val;
}w[N*2];
int vis[N],tot=0,head[N],now[N],cnt=0;
int f[N][23],dep[N],id[N],pos[N];LL valuee[N];
LL ans=0;
set<int>s;
void add(int a,int b,int c)
{
    tot++;
    w[tot].to=b;
    w[tot].nextt=head[a];
    w[tot].val=c;
    head[a]=tot;
}
void dfs(int x)
{
    id[++cnt]=x;
    pos[x]=cnt;
    for(int i=head[x];i;i=w[i].nextt)
    {
        int v=w[i].to;
        if(v==f[x][0])continue;
        f[v][0]=x;valuee[v]=valuee[x]+w[i].val;
        for(int i=1;i<=20;++i)
          f[v][i]=f[f[v][i-1]][i-1];
        dep[v]=dep[x]+1;
        dfs(v);
    }
}
int lca(int x,int y)
{
    if(dep[x]<dep[y])swap(x,y);
    for(int i=20;i>=0;--i)if(dep[f[x][i]]>=dep[y])x=f[x][i];
    if(x==y)return x;
    for(int i=20;i>=0;--i)if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
    return f[x][0];
}
LL dis(int x,int y)
{
    LL res=0;
    int LCA=lca(x,y);
    res+=valuee[x]+valuee[y]-2*valuee[LCA];
    return res;
}
void modify(int x)
{
    now[x]^=1;
    if(now[x]) s.insert(pos[x]);
    else s.erase(pos[x]);
    if(s.size()<=1){ans=0;return;}//存在只有一个和没有的情况,都要直接返回,这个时候就已经不存在前驱后继了 
    set<int>::iterator pre,nxt;
    pre=s.lower_bound(pos[x]);//大于等于
    nxt=pre;
    if(now[x])nxt++;
    if(pre==s.begin())pre=--s.end();
    else pre--;
    if(nxt==s.end()) nxt=s.begin();
    LL dis1=dis(id[*pre],id[*nxt]);
    LL dis2=dis(id[*pre],x);
    LL dis3=dis(x,id[*nxt]);
    if(now[x])ans+=dis2+dis3-dis1;
    else ans-=dis2+dis3-dis1;
}
int main()
{
    int n=read(),m=read();
    for(int i=1;i<n;++i)
    {
        int a=read(),b=read(),c=read();
        add(a,b,c);add(b,a,c);
    }
    dep[1]=1,dfs(1);
    for(int i=1;i<=m;++i)
    {
        int x=read();
        modify(x);
        printf("%lld
",ans);
    }
} 
BZOJ3991

然后看一下考试时遇到的有一点小变化的题

题面

初音未来的巡游

128MB / 1s ; cruise.cpp / c / pas / in / out

【题目描述】

Miku决定在n个地点中选择一些地点进行巡游。这n个地点由n-1条道路连接,两两之间有且仅有一条路径可以互相到达。Miku希望在这些道路中选择一些放上葱,使得Miku可以选择一种方式只经过有葱的道路而巡游完所有她选择的地点(一条道路可以被多次经过,起点任选)。

Miku想知道她至少需要准备多少葱。由于她的巡游计划可能更改,她希望你能在更改后即时回答她的疑问。

 

【输入格式】

第一行两个整数n,m,表示地点数和事件数。

第2至n行,每行两个整数x,y,表示地点x和地点y之间有一条无向道路。

接下来一行n个0/1数,若第i个数为1则表示i号地点初始时被选,为0则表示不选。

接下来一行m个整数,依次表示修改事件。第i个数Ai表示Ai号地点的状态发生变化,即若当前被选则改为不选,当前不选则改为被选。

 

【输出格式】

输出m行,第i行一个整数表示第i次事件后初音最少需要准备多少葱。

 

【样例数据】

 

cruise.in

cruise.out

5 8

1 2

1 3

2 4

2 5

1 0 0 1 0

5 4 2 1 2 5 3 2

3

2

2

1

0

0

0

2

 

【数据范围】

对于30%的数据,n,m≤3000。

对于另30%的数据,开始时所有地点都不选,保证修改操作的地点当前是不选状态。

对于100%的数据,1≤n,m≤200000,1≤x,y,Ai≤n。


不同的只是边权值全部为1,且只需要统计走过的路径条数,而不是走过的距离,简单分析我们可以发现,其实就是上一道题的ans/2,因为原来是走过了还要走回去,相当于走两次。

#include<bits/stdc++.h>
#define N 200003
#define LL long long
using namespace std;
int read()
{
    int x=0,f=1;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    return x*f;
}
struct EDGE{
    int to,nextt;
}w[N*2];
int vis[N],tot=0,head[N],now[N],cnt=0;
int f[N][23],dep[N],id[N],pos[N];
int ans=0;
set<int>s;
void add(int a,int b)
{
    tot++;
    w[tot].to=b;
    w[tot].nextt=head[a];
    head[a]=tot;
}
bool dfs(int x)
{
    id[++cnt]=x;
    pos[x]=cnt;
    bool back=now[x];
    for(int i=head[x];i;i=w[i].nextt)
    {
        int v=w[i].to;
        if(v==f[x][0])continue;
        f[v][0]=x;
        for(int i=1;i<=20;++i)
          f[v][i]=f[f[v][i-1]][i-1];
        dep[v]=dep[x]+1;
        if(dfs(v))//说明fa为v这条路径是要放葱的,因为下面有要选择的点,而到那个点的路径唯一,必须经过fa为v这条路 
        {
            ans++;//放葱的路径数++ 
            back=1;
        }
    }
    return back;
}
int lca(int x,int y)
{
    if(dep[x]<dep[y])swap(x,y);
    for(int i=20;i>=0;--i)if(dep[f[x][i]]>=dep[y])x=f[x][i];
    if(x==y)return x;
    for(int i=20;i>=0;--i)if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
    return f[x][0];
}
int dis(int x,int y)
{
    int LCA=lca(x,y);
    return dep[x]+dep[y]-2*dep[LCA];
}
void modify(int x)
{
    
    now[x]^=1;
    if(now[x]) s.insert(pos[x]);
    else s.erase(pos[x]);
    if(s.size()<=1){ans=0;return;}//存在只有一个和没有的情况,都要直接返回 
    set<int>::iterator pre,nxt;
    pre=s.lower_bound(pos[x]);//大于等于 
    nxt=pre;
    if(now[x])nxt++;
    if(pre==s.begin())pre=--s.end();
    else pre--;
    if(nxt==s.end()) nxt=s.begin();
    int dis1=dis(id[*pre],id[*nxt]);
    int dis2=dis(id[*pre],x);
    int dis3=dis(x,id[*nxt]);
    if(now[x])ans+=dis2+dis3-dis1;
    else ans-=dis2+dis3-dis1;
}
int main()
{
    freopen("cruise.in","r",stdin);
    freopen("cruise.out","w",stdout);
    int n=read(),m=read();
    for(int i=1;i<n;++i)
    {
        int a=read(),b=read();
        add(a,b);add(b,a);
    }
    int go=0;
    for(int i=1;i<=n;++i)
    {
        now[i]=read();
        if(now[i])go=i;
    }
    if(!go)dep[1]=1,dfs(1);
    else dep[go]=1,dfs(go);//从一个被选择的点开始走一定更优 
    ans=ans*2;
    for(int i=1;i<=n;++i)
      if(now[i]) s.insert(pos[i]);
    for(int i=1;i<=m;++i)
    {
        int x=read();
        modify(x);
        printf("%d
",ans/2);
    }
} 
View Code

原文地址:https://www.cnblogs.com/yyys-/p/11260981.html