Codeforces Gym 101741 部分题题解

C

简要题意

给定一棵n个点的树,再给定m对点,每对点表示一条路径。求一个最小点集,使得每条路径上都存在点集中的点。

数据范围

\(1\leq n,m\leq 10^5\)

简要题解

据说树上路径问题常常和LCA有关,于是我们对于每一条路径,考虑它的LCA。
对于LCA最深的那一条路径,一定把LCA放入点集是最优的。因为该路径上必须有一个点在点集内。
如果该路径与其他路径无交,那么取LCA肯定没问题。如果该路径与其他路径有交,那么LCA一定是交点之一,因为当前路径的LCA最深,而其他路径想要与当前路径有交,必须经过LCA才能下来。
那么就有一个贪心策略了:我们优先考虑LCA最深的路径,若该路径未被标记,则将LCA加入点集,并把经过LCA的所有路径标记。
具体实现的话,对于树上的每个点,我们维护一个set。对于每一条路径,我们将路径的标号扔进路径端点的set中,表示该路径未被标记。
然后我们Dfs,对于当前点,先遍历子树,然后将子树的set和当前点的set合并。如果合并过程中出现了重复元素,说明当前点是该元素对应路径的LCA,且该路径未被标记,那么我们就把这个点放入点集,然后清空该点的set,表示当前所有路径都被标记。
为了保证复杂度,合并过程采用启发式合并。
时间复杂度\(O(nlog^2n)\),set一个log,启发式合并一个log
代码如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+10;
struct EDGE{   int u,v,Next;   }Edge[MAXN<<1];
int n,m,Es,Ans;
int First[MAXN],Bel[MAXN],Have[MAXN];
set<int>Set[MAXN];
int Read()
{   int a=0,c=1;   char b=getchar();
    while(b!='-'&&(b<'0'||b>'9')) b=getchar();
    if(b=='-') c=-1,b=getchar();
    while(b>='0'&&b<='9') a=a*10+b-48,b=getchar();
    return a*c;
}
void Link(int u,int v){   Edge[++Es]=(EDGE){u,v,First[u]},First[u]=Es;   }
int Merge(int A,int B)
{   if(Set[A].size()<Set[B].size()) swap(A,B);
    for(int Ele:Set[B])
        if(Set[A].find(Ele)!=Set[A].end()) return Set[A].clear(),Set[B].clear(),-1;
        else Set[A].insert(Ele);
    return A;
}
int Dfs(int Now,int Ba)
{   //printf("Now=%d Ba=%d\n",Now,Ba);
    for(int i=First[Now],v;i!=-1;i=Edge[i].Next)
        if((v=Edge[i].v)!=Ba) Dfs(v,Now);
    if(Have[Now]) return Set[Bel[Now]].clear(),Ans++;
    for(int i=First[Now],v,New;i!=-1&&!Have[Now];i=Edge[i].Next)
    {   if((v=Edge[i].v)==Ba) continue ;
        if((New=Merge(Bel[Now],Bel[v]))==-1) Have[Now]=1,Ans++;
        else Bel[Now]=New;
    }
    //printf("Finish %d\n",Now);
}
int main()
{   n=Read(),memset(First,-1,sizeof(First));
    for(int i=1,u,v;i<n;i++) u=Read(),v=Read(),Link(u,v),Link(v,u);
    m=Read();
    for(int i=1,A,B;i<=m;i++) A=Read(),B=Read(),A==B?Have[A]=1:(Set[A].insert(i),Set[B].insert(i),0);
    for(int i=1;i<=n;i++) Bel[i]=i;
    Dfs(1,1),printf("%d\n",Ans);
    for(int i=1;i<=n;i++) Have[i]?printf("%d ",i):0;
}

G.Berland Post

题目链接

Berland Post

简要题解

不难发现,题目中的\(T\)具有可二分性,即\(\exist T_0\in R\) \(s.t. T< T_0\)时不存在合法方案,\(T>T_0\)时必存在合法方案。
因此我们只需要二分\(T\),然后判断是否存在合法方案即可。

我们的约束条件形如\(o_i+d\leq o_j+T\),移项则变成\(o_i \leq o_j+T-d\),这是差分约束的经典模型。
现在考虑建图跑差分约束,以\(0\)号点为原点,\(o_0=0\)
如果\(o_i\)是固定的,我们连一条权值为\(o_i\)\(0->i\)有向边,再连一条权值为\(-o_i\)\(i->0\)有向边。
对于每一个形如\(o_i\leq o_j+T-d\)的约束条件,我们连一条权值为\(T-d\)\(j->i\)有向边。
然后跑\(SPFA\),无解的话就是出现了负环,如果某个元素在\(SPFA\)过程中入队超过了\(n\)次,那么这张图就存在负环。
图中有一些连通块和原点不连通,对其中任意一个点赋上一个合适的初值即可。
代码如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1010;
const double EPS=1e-8;
const double Inf=1e9;
struct EDGE{   int u,v,Next;   double w;   }Edge[MAXN*10];
struct ELE{   int A,B,D;   }Lim[MAXN*2];
double Ans,Open[MAXN],Dis[MAXN];
int n,m,Es,First[MAXN],Sure[MAXN];
int Vis[MAXN],Pass[MAXN];
queue<int>Team;
int Read()
{   int a=0,c=1;   char b=getchar();
    while(b!='-'&&b!='?'&&(b<'0'||b>'9')) b=getchar();
    if(b=='?') return Inf;
    if(b=='-') c=-1,b=getchar();
    while(b>='0'&&b<='9') a=a*10+b-48,b=getchar();
    return a*c;
}
void Link(int u,int v,double w){   Edge[++Es]=(EDGE){u,v,First[u],w},First[u]=Es;   }
void Init()
{   for(int i=0;i<=n;i++) First[i]=-1,Dis[i]=Inf,Vis[i]=Pass[i]=0;
    while(!Team.empty()) Team.pop();
    Es=0;
}
int Check(double Nt)
{   Init(),Dis[0]=0;
    for(int i=1;i<=n;i++)
        if(Sure[i]) Link(0,i,Open[i]),Link(i,0,-Open[i]);
    for(int i=1;i<=m;i++) Link(Lim[i].B,Lim[i].A,Nt-Lim[i].D);
    for(int i=0;i<=n;i++) Team.push(i),Vis[i]=1;
    for(int Now;!Team.empty();)
    {   Now=Team.front(),Team.pop(),Vis[Now]=0;
        for(int i=First[Now],v;i!=-1;i=Edge[i].Next)
        {   double w=Edge[i].w;
            if(Dis[v=Edge[i].v]<=Dis[Now]+w) continue ;
            Dis[v]=Dis[Now]+w;
            if(Vis[v]) continue ;
            if((++Pass[v])>n) return 0;
            Vis[v]=1,Team.push(v);
        }
    }
    for(int i=1;i<=n;i++) Open[i]=Dis[i];
    return 1;
}
int main()
{   while(scanf("%d%d",&n,&m)!=EOF)
    {   for(int i=1;i<=n;i++) Open[i]=Read(),Sure[i]=Open[i]<1e8;
        for(int i=1;i<=m;i++) scanf("%d%d%d",&Lim[i].A,&Lim[i].B,&Lim[i].D);
        for(double Le=0,Ri=1e7,Mid;Le+EPS<Ri;)
            Mid=(Le+Ri)/2,Check(Mid)?Ri=Ans=Mid:Le=Mid;
        printf("%.8lf\n",Ans);
        for(int i=1;i<=n;i++) printf("%.8lf ",Open[i]);
        puts("");
    }
}

L

简要题意

给定一张n个点m条边有边权的无向连通图,对于每一条边,询问有多少个点,满足这些点到1号点的最短路必须经过该边。

数据范围

\(1\leq n\leq 2*10^5\),\(n-1\leq m\leq 2*10^5\),边权\(1\leq w_i\leq 10^9\)

简要题解

好像是灭绝树的相关算法,不过这里只关心怎么写本题。
很明显首先要用Dijkstra求出最短路。
然后,我们需要找到哪些边在最短路上,并新建一张图。
具体地说,若第\(k\)条边的两端为\(i\)\(j\),且\(Dis[i]=Dis[j]+w\),那么在新图上我们建一个新点v来表示第k条边,且连上\(j\)->\(v\),和\(v\)->\(i\)
的有向边。
在新图上,所有走到1号点的路径都是最短路,且原图中的所有最短路都在新图上。
接下来我们要建一棵树,这棵树大概满足这样的性质:删掉某一节点后,在新图中无法走到根节点的那些点,必定在该节点的子树内。
如果建好了这棵树,那么每条边的答案其实就是对应点在这棵树上的子树大小(代表边的点不算入大小)。
建树的话,每次加入一个点,在树内找一个父亲。
如何找这个父亲?我们知道当前点在新图中直接连向哪些节点,只要对这些节点求一个公共的LCA,作为当前点的父亲即可。
求LCA可以用倍增,支持在下端插入新点。
感性理解,如果删的不是公共祖先,那么一定存在一条路径通向根节点,如果不是最近公共祖先,那么不满足这棵树的性质。
所以我们想把某个点加入树中,就必须把它指向的那些点先加入树中,这个顺序关系在新图中做个拓扑排序就可以了。
代码如下:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+10;
const ll Inf=1e18;
struct EDGE{   int u,v,w,Id,Next;   }Edge[MAXN*2];
int n,m,Es,Ts,Top;
int First[MAXN],Topo[MAXN*2],Size[MAXN*2];
ll Dis[MAXN];
vector<int>Last[MAXN*2];
int Read()
{   int a=0,c=1;   char b=getchar();
    while(b!='-'&&(b<'0'||b>'9')) b=getchar();
    if(b=='-') c=-1,b=getchar();
    while(b>='0'&&b<='9') a=a*10+b-48,b=getchar();
    return a*c;
}
void Link(int u,int v,int w,int Id){   Edge[++Es]=(EDGE){u,v,w,Id,First[u]},First[u]=Es;   }
namespace DIJ
{   struct ELE{   ll Dis;   int Pos;   };
    priority_queue<ELE>Heap;
    int Vis[MAXN];
    bool operator < (ELE A,ELE B){   return A.Dis==B.Dis?A.Pos<B.Pos:A.Dis>B.Dis;   }
    void Dijkstra()
    {   for(int i=1;i<=n;i++) Dis[i]=Inf;
        Dis[1]=0,Heap.push((ELE){0,1});
        for(int Now;!Heap.empty();)
        {   Now=Heap.top().Pos,Heap.pop(),Vis[Now]++;
            if(Vis[Now]>1) continue ;
            for(int i=First[Now],v,w;i!=-1;i=Edge[i].Next)
                if(Dis[v=Edge[i].v]>Dis[Now]+(w=Edge[i].w)) Dis[v]=Dis[Now]+w,Heap.push((ELE){Dis[v],v});
        }
    }
}using DIJ::Dijkstra;
namespace TOPO
{   int Deg[MAXN*2];
    queue<int>Team;
    void Build()
    {   for(int i=1;i<=n;i++)
            for(int j=First[i];j!=-1;j=Edge[j].Next)
            {   int v=Edge[j].v,w=Edge[j].w,Nd=Edge[j].Id+n;
                if(Dis[v]!=Dis[i]+w) continue ;
                Last[v].push_back(Nd),Last[Nd].push_back(i),Deg[i]++,Deg[Nd]++;
            }
    }
    void Toposort()
    {   for(int i=1;i<=n;i++)
            if(Deg[i]==0) Team.push(i);
        for(int Now;!Team.empty();)
        {   Now=Team.front(),Team.pop(),Topo[++Ts]=Now;
            for(int v:Last[Now])
                if((--Deg[v])==0) Team.push(v);
        }
    }
}using TOPO::Toposort;using TOPO::Build;
namespace TREE
{   int Deep[MAXN*2],Fa[MAXN*2][21];
    int Get_Lca(int A,int B)
    {   if(A==0||B==0) return A+B;
        if(Deep[A]<Deep[B]) swap(A,B);
        for(int i=0,Aim=Deep[A]-Deep[B];i<=20;i++)
            if(Aim>>i&1) A=Fa[A][i];
        if(A==B) return A;
        for(int i=20;i>=0;i--)
            if(Fa[A][i]!=Fa[B][i]) A=Fa[A][i],B=Fa[B][i];
        return Fa[A][0];
    }
    void Get_Fa(int S)
    {   int Lca=0;
        for(int i:Last[S]) Lca=Get_Lca(Lca,i);
        Fa[S][0]=Lca,Deep[S]=Deep[Lca]+1,Size[S]=(S<=n);
        for(int i=1;i<=Top;i++) Fa[S][i]=Fa[Fa[S][i-1]][i-1];
    }
    void Die()
    {   for(int i=Ts;i>=1;i--) Get_Fa(Topo[i]);
        for(int i=1,S;i<=Ts;i++) S=Topo[i],Size[Fa[S][0]]+=Size[S];
    }
}using TREE::Die;
int main()
{   n=Read(),m=Read(),Top=log2(n+m);
    memset(First,-1,sizeof(First));
    for(int i=1,u,v,w;i<=m;i++) u=Read(),v=Read(),w=Read(),Link(u,v,w,i),Link(v,u,w,i);
    Dijkstra(),Build(),Toposort(),Die();
    for(int i=1;i<=m;i++) printf("%d\n",Size[i+n]);
}
posted @ 2021-07-23 10:52  Alkaid~  阅读(126)  评论(0编辑  收藏  举报