题解:P5680 [GZOI2017] 共享单车

· · 题解

前置知识

题意简述

其它题解其实写的相当详尽了,概括一下就是:

思路

Code

觉得难理解的可以看看这篇含详细注释的代码 awa。

#include<bits/stdc++.h>
using namespace std;
const int N=50010,M=100010,MAXN=50010;

int n,m,k,q;
int val[N];//记录是否被标记 
bool flag;//记录是否找到被标记投放区域 
int r[MAXN],num;//r回收区域编号 
int h[N],to[M<<1],len[M<<1],nxt[M<<1],cnt;//链式前向星

void addedge(int u,int v,int l)//建边 
{
    cnt++;
    nxt[cnt]=h[u];
    h[u]=cnt;
    to[cnt]=v;
    len[cnt]=l;
}

int pre[N],Len[N];//pre[i]记录节点i的前一个节点 Len[i]记录i和pre[i]的距离,方便之后重利用之前的链式前向星
int dis[N];//Dijkstra中当前最短距离 

void Find_Tree()//Dijkstra求以k为根的最小路径树 
{
    memset(dis,-1,sizeof(dis));
    priority_queue<pair<int,int>> q;//Dijkstra堆优化 
    //记得priority_queue默认大根堆,求最小路径树可以插入路径长度的相反数 
    q.push(make_pair(0,k));
    dis[k]=0;
    while(!q.empty())
    {
        int d=-q.top().first,u=q.top().second;
        q.pop();
        for(int i=h[u];i;i=nxt[i])
        {
            int v=to[i];
            if(dis[v]==-1||dis[v]>dis[u]+len[i])//取路径长度较短
            {
                pre[v]=u;
                Len[v]=len[i];
                dis[v]=dis[u]+len[i];
                q.push(make_pair(-dis[v],v));
            }
            else if(dis[v]==dis[u]+len[i]&&pre[v]>u)//路径长度相同取编号较小
            {
                pre[v]=u;
                Len[v]=len[i];
            }
        }
    }
    //清空原来的链式前向星,用于记录新的最小路径树 
    memset(h,0,sizeof(h));
    cnt=0;
    for(int i=1;i<=n;i++)
        if(i!=k)
        {
            addedge(pre[i],i,Len[i]);
            addedge(i,pre[i],Len[i]);
        }
}

int dfn[N],DFN;//时间戳 
int dep[N],f[N][16];//dep[i]记录节点i在树上深度 f记录祖先 

void dfs(int u,int fa){//LCA预处理 
    dfn[u]=++DFN;//同时记录时间戳 
    dep[u]=dep[fa]+1;
    for(int i=1;i<=15;i++) f[u][i]=f[f[u][i-1]][i-1];
    for(int i=h[u];i;i=nxt[i])
    {
        int v=to[i];
        if(v==fa) continue;
        f[v][0]=u;
        dfs(v,u);
    }
}

int lca(int x,int y){//求x和y的LCA 
    if(dep[x]<dep[y]) swap(x,y);
    for(int i=15;i>=0;i--)
    {
        if(dep[f[x][i]]>=dep[y]) x=f[x][i];
        if(x==y) return y;
    }
    for(int i=15;i>=0;i--)
        if(f[x][i]!=f[y][i])
        {
            x=f[x][i];
            y=f[y][i];
        }
    return f[x][0];
}

//虚树点数为 实点+虚点 数组要开两倍 
int vt[MAXN<<1],vtcnt;//vt存储虚树中的点 vtcnt虚树节点个数 
vector<int> VT[MAXN<<1];//VT vector存虚树 

bool cmp(int a,int b){ return dfn[a]<dfn[b]; }//根据 dfn 序排序 

void Virtual_Tree()//建虚树 
{
    vtcnt=0;
    sort(r+1,r+1+num,cmp);//第一次排序 
    for(int i=1;i<num;i++)
    {
        vt[++vtcnt]=r[i];
        vt[++vtcnt]=lca(r[i],r[i+1]);//插入LCA 
    }
    vt[++vtcnt]=r[num];
    sort(vt+1,vt+1+vtcnt,cmp);//第二次排序 
    vtcnt=unique(vt+1,vt+1+vtcnt)-vt-1;//去重 
    for(int i=1;i<vtcnt;i++)
    {
        int l=lca(vt[i],vt[i+1]);
        VT[l].push_back(vt[i+1]);//连边 
    }
}

int calc(int u)//虚树上跑树形DP求最小代价 
{
    int res=0;
    if(val[u]) flag=1;
    int siz=VT[u].size();
    for(int i=0;i<siz;i++)
    {
        int v=VT[u][i];
        int tmp=calc(v);
        if(val[v]) res+=dis[v]-dis[u];//被标记时必须阻断这条路径
        else res+=min(dis[v]-dis[u],tmp);//无标记时选择截断该路径或继承 v 的 DP 值 
    }
    VT[u].clear();
    return res;
}

int main()
{
    cin>>n>>m>>k>>q;
    for(int i=1;i<=m;i++)
    {
        int u,v,l;
        scanf("%d%d%d",&u,&v,&l);
        addedge(u,v,l);
        addedge(v,u,l);
    }
    Find_Tree();
    dfs(k,0);
    while(q--)
    {
        int opt;
        scanf("%d%d",&opt,&num);
        if(!opt)//opt==0 区域标记异或取反 
        {
            for(int i=1;i<=num;i++)
            {
                int p;
                scanf("%d",&p);
                val[p]^=1;
            }
        }
        else//opt==1 建虚树 + DP
        {
            for(int i=1;i<=num;i++) scanf("%d",&r[i]);
            r[++num]=k;
            Virtual_Tree();
            flag=0;
            int ans=calc(k);
            if(flag) printf("%d\n",ans);
            else printf("-1\n");
        }
    }
    return 0;
}

Tips:撰文不易,不适轻喷。