「笔记」图论杂题选做

1. NOIP 2014 联合权值

题目大意:一棵 n 个点的树,每一个点有一个点权。若两个点距离为 2,那么它们就会产生它们的点权之积的联合权值。求整棵树的联合权值之和。1≤n≤2×105

Solution:

若距离为 2,则一定存在一个中转点。

枚举这个中转点,周围点权之积可以直接算。

假设每个中转点周围有两个点,权值分别为 a、b,则联合权值为 2ab=(a+b)2-(a2+b2)。

若有三个点,权值分别为 a、b、c,则联合权值为 2ab+2bc+2ac=(a+b+c)2-(a2+b2+c2)。

综上,以某个节点为中转点的联合权值之和等于权值和的平方减去权值的平方和。

为了找到最大的联合权值,只需找到周围最大的两个权值 mx1,mx2,相乘判断即可。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5,mod=10007;
int n,x,y,w[N],cnt,hd[N],to[N<<1],nxt[N<<1],k1,k2,mx1,mx2,ans1,ans2;
void add(int x,int y){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
}
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<n;i++){
        scanf("%lld%lld",&x,&y);
        add(x,y),add(y,x);
    } 
    for(int i=1;i<=n;i++)
        scanf("%lld",&w[i]);
    for(int x=1;x<=n;x++){
        mx1=mx2=k1=k2=0;
        for(int i=hd[x];i;i=nxt[i]){
            int y=to[i];
            if(w[y]>mx1) mx2=mx1,mx1=w[y];
            else if(w[y]>mx2) mx2=w[y];
            k1=(k1+w[y]%mod)%mod,k2=(k2+w[y]%mod*w[y]%mod)%mod;
        }
        k1=k1*k1%mod,ans2=(ans2+k1-k2+mod)%mod,ans1=max(ans1,mx1*mx2);
    }
    printf("%lld %lld\n",ans1,ans2);
    return 0;
}

2. NOI 2001 食物链

题目大意:

有三种生物 A,B,C,A 吃 B,B 吃 C,C 吃 A。现在有 n 个生物,还有 k 个条件,每一个条件形如下面一种:

  • X 和 Y 是同类。
  • X 吃 Y。

问你有多少句话和前面的话冲突。n≤5×105,k≤105

Solution:

设 X1, X2, X3 分别表示自己,食物和天敌,用 x, x+n, x+2*n 表示。

用并查集维护,如果在任何时刻 X1, X2, X3 在同一个集合中那就不合法。

时间复杂度 O(kα(n))。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5; 
int n,k,x,y,z,ans,f[N];
int get(int x){
    return f[x]==x?x:f[x]=get(f[x]);
}
signed main(){
    scanf("%lld%lld",&n,&k);
    for(int i=1;i<=3*n;i++) f[i]=i;
    for(int i=1;i<=k;i++){
        scanf("%lld%lld%lld",&z,&x,&y);
        if(x>n||y>n) ans++;
        else if(z==1){
            if(get(x)==get(y+n)||get(x)==get(y+2*n)){ans++;continue;}
            f[get(f[x])]=get(f[y]);
            f[get(f[x+n])]=get(f[y+n]);
            f[get(f[x+2*n])]=get(f[y+2*n]);
        }
        else if(z==2){
            if(x==y||get(x)==get(y)||get(x)==get(y+n)){ans++;continue;}
            f[get(f[x])]=get(f[y+2*n]);
            f[get(f[x+n])]=get(f[y]);
            f[get(f[x+2*n])]=get(f[y+n]);
        }
    }
    printf("%lld\n",ans);
    return 0;
}

3. NOIP 2013 车站分级

题目大意:一条单向的铁路线上,依次有编号为 1,2,...,n 的 n 个火车站。每个火车站都有一个级别,最低为 1 级。现有若干趟车次在这条线路上行驶,每一趟都满足如下要求:
如果这趟车次停靠了火车站 x,则始发站、终点站之间所有级别大于等于火车站 x 的都必须停靠。(注意:起始站和终点站自然也算作事先已知需要停靠的站点)。现有 m 趟车次的运行情况(全部满足要求),问这 n 个火车站至少分为几个不同的级别。n,m≤1000。

Solution:

对于一列火车,停靠的所有车站级别一定大于未停靠的,相当于拓扑排序中的偏序关系。由于一定满足条件,因此一定能得到一个有向无环图。

拓扑排序的时候顺带进行 DP,设 f[i] 表示 i 的最小级别。

直接暴力建图,时间复杂度 O(mn2)。

可以引入辅助点,时间复杂度降到 O(nm)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e3+5,M=1e6+5;
int n,m,x,a[N],cnt,hd[N],to[M],nxt[M],f[N],in[N],ans;
bool vis[N],mp[N][N];
queue<int>q;
void add(int x,int y){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
}
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%lld",&x);
        memset(vis,0,sizeof(vis));
        for(int j=1;j<=x;j++)
            scanf("%lld",&a[j]),vis[a[j]]=1;
        for(int j=a[1];j<=a[x];j++) 
            if(!vis[j]) for(int k=1;k<=x;k++) 
                if(!mp[a[k]][j]) mp[a[k]][j]=1,in[j]++,add(a[k],j);    //把一个车次中停靠的车站与一个车次的所有不停靠的车站连边,前提是没有连过,再将不停靠的车站入读加一 
    }
    for(int i=1;i<=n;i++)
        if(!in[i]) q.push(i);
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=hd[x];i;i=nxt[i]){
            int y=to[i];
            if(--in[y]==0) f[y]=f[x]+1,q.push(y);
        }
    }
    for(int i=1;i<=n;i++)
        ans=max(ans,f[i]);
    printf("%lld",ans+1);
    return 0;
}

4.  Luogu P4643 阿狸和桃子的游戏

题目大意:有一张 n 个点 m 条边的图,点有点权,边有边权。先手后手轮流染黑白两色,最后的得分是自己染的点权和 + 两端均为自己的颜色的边权和。双方都希望自己的得分-对手的得分最大,求结果。1≤n≤104,0≤m≤105

Solution:

把一个点 u 的点权变为 2w(u)+∑w(u,v),边权变为 0。

接下来贪心选取。

可以发现同色边权不会发生变化,异色边权两方分数可以相互抵消。

时间复杂度 O(n log n)。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int n,m,a[N],x,y,z,ans;
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%lld",&a[i]),a[i]*=2;
    for(int i=1;i<=m;i++){
        scanf("%lld%lld%lld",&x,&y,&z);
        a[x]+=z,a[y]+=z;
    }
    sort(a+1,a+1+n);
    for(int i=n;i>=1;i-=2)
        ans+=a[i]-a[i-1];
    printf("%lld\n",ans/2);
    return 0;
}

5. NOIP 2015 运输计划

题目大意:一棵 n 个点的树,给你 m 条路径。你需要选出一条边把边权改成 0,让这些路径长度的最大值最小。n,m≤3×105

Solution:

最大值最小,先二分。

二分之后,有一些路径的长度已经满足要求了,直接忽略。

剩下的路径由于太长,所以每一条路径都必须经过被删除的边。

假设有 k 条超过限制的路径,对于每一条路径,我们把这条路径上所有边的 cnt+1 ,然后看被所有 cnt=k 的边,删掉其中最长的边,看是否能满足条件。

对于 cnt+1 的操作,我们可以用树上差分实现。每次 cnt[u]++,cnt[v]++,cnt[lca]-=2。

时间复杂度 O((n+m) log n),有点卡常。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
int n,m,x,y,z,cnt,hd[N],to[N<<1],nxt[N<<1],val[N<<1],u[N],v[N],lca[N],dis[N],dep[N],d[N],f[N][30],sum,b[N],top,l,r,mid,ans,c[N];
int add(int x,int y,int z){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt,val[cnt]=z;
} 
void dfs(int x,int fa){
    b[++top]=x,dep[x]=dep[fa]+1;    //b 数组的作用:找到叶节点向上累加
    for(int i=0;i<=19;i++)
        f[x][i+1]=f[f[x][i]][i];
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        if(y==fa) continue;
        f[y][0]=x,d[y]=d[x]+val[i],dfs(y,x);
    }
}
int LCA(int x,int y){    //倍增求 LCA
    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];
}
bool solve(int mid){
    int k=0,ans=0;
    memset(c,0,sizeof(c));    //cnt 数组 
    for(int i=1;i<=m;i++)
        if(dis[i]>mid){
            c[u[i]]++,c[v[i]]++,c[lca[i]]-=2;    //树上差分 
            ans=max(ans,dis[i]-mid),k++;
        }
    if(!k) return 1;
    for(int i=n;i>=1;i--)
        c[f[b[i]][0]]+=c[b[i]];
    for(int i=2;i<=n;i++)
        if(c[i]==k&&d[i]-d[f[i][0]]>=ans) return 1;    //找出被所有超过限制的路径覆盖的最长路径
    return 0;
}
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<n;i++){
        scanf("%lld%lld%lld",&x,&y,&z);
        add(x,y,z),add(y,x,z),sum+=z;
    }
    dfs(1,0);
    for(int i=1;i<=m;i++){
        scanf("%lld%lld",&u[i],&v[i]);
        lca[i]=LCA(u[i],v[i]),dis[i]=d[u[i]]+d[v[i]]-2*d[lca[i]];    //点x与点y之间的距离=x的深度+y的深度-2*LCA(x,y)的深度 
    }
    l=0,r=sum;
    while(l<=r){
        mid=(l+r)/2;
        if(solve(mid)) ans=mid,r=mid-1;
        else l=mid+1;
    }
    printf("%lld\n",ans);
    return 0;
}

6. CF1349C Orac and Game of Life

题目大意:有一个 n×m 的网格,每一个格子初始是黑或白两种颜色。

接下来的每一个时刻,对于一个格子 (i,j):

  • 若相邻的格子中有至少一个格子和它的颜色相同,那么这个格子的颜色取反。
  • 否则,颜色不变。

q 次询问,每次问你 (x,y) 在第 t 时刻的颜色。1≤n,m≤103,1≤q≤105,1≤t≤1018

Solution:

首先我们找规律。

注意到,一旦某个连通块的大小超过 1,那么它就会无限 01 反转下去。

而如果周围有大小为 1 的连通块,那么这两个连通块就会合并,然后继续一起反转。

对于每一个格子,我们预处理一个 ci,j,表示这个位置在这个时刻就会进入一个大小大于 1 的连通块。

对于询问,如果 t≤ci,j 那就不变,否则就黑白交错出现。

由于边权都是 1,所以可以直接 bfs。时间复杂度 O(nm+q)。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e3+5;
int n,m,t,a[N][N],x,y,p,d[N][N],dx[4]={1,-1,0,0},dy[4]={0,0,1,-1};
bool vis[N][N],flag;
queue<pair<int,int> >q;
signed main(){
    scanf("%lld%lld%lld",&n,&m,&t);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            scanf("%1lld",&a[i][j]);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            for(int k=0;k<4;k++){
                int xx=i+dx[k],yy=j+dy[k];
                if(xx>=1&&xx<=n&&yy>=1&&yy<=m&&a[xx][yy]==a[i][j]) vis[i][j]=1;
            }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(vis[i][j]) q.push(make_pair(i,j));
    if(!q.size()) flag=1;
    while(q.size()){
        int xx=q.front().first,yy=q.front().second; q.pop();
        for(int k=0;k<4;k++){
            int nx=xx+dx[k],ny=yy+dy[k];
            if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&!vis[nx][ny]) vis[nx][ny]=1,d[nx][ny]=d[xx][yy]+1,q.push(make_pair(nx,ny));
        }
    }
    while(t--){
        scanf("%lld%lld%lld",&x,&y,&p);
        if(flag||p<d[x][y]) printf("%lld\n",a[x][y]);
        else printf("%lld\n",(a[x][y]+p-d[x][y])%2);
    }
    return 0;
}

7. CF209C Trails and Glades

题目大意:给一张图,添加最少的边使得整张图存在一条欧拉回路。1≤n106,0≤m≤106

Solution:

连通图中存在欧拉回路的充要条件是该图所有顶点的度数都为偶数。一个显然的想法就是:找出所有度数为奇数的点,然后将这些点两两相连,得到的图的度数就全部是偶数了。

但是这样并不对,因为存在欧拉回路需要先保证原图是一个连通图。所以,我们在连边的过程中还需要保证新图连通。

考虑每一个连通块。

如果只有一个连通块,那就直接把度数为奇数的点两两相连。答案为奇数点个数除以 2。

否则如果有 k 个连通块,我们需要先把所有连通块连接。

  • 若某个连通块不存在奇数点(即所有节点的度数均为偶数),因为如果从这个连通块向别的连通块只连一条边就会出现奇数点,所以应该从这个连通块向别的连通块连两条边。
  • 若某个连通块存在奇数点,则保留两个奇数点,从这两个奇数点向外连两条边。并不影响答案。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int n,m,x,y,fx,fy,cnt,f[N],d[N],c[N];
int find(int x){
    return f[x]==x?x:f[x]=find(f[x]);
}
signed main(){
    scanf("%lld%lld",&n,&m),d[1]=2;
    for(int i=1;i<=n;i++) f[i]=i;
    for(int i=1;i<=m;i++){
        scanf("%lld%lld",&x,&y);
        d[x]++,d[y]++;
        x=find(x),y=find(y),f[x]=y;
    }
    for(int i=1;i<=n;i++)
        if(d[i]&1) cnt++,c[find(i)]=1;
    for(int i=1;i<=n;i++)
        if(f[i]==i&&d[i]&&!c[i]) cnt+=2;
    if(!c[f[1]]&&cnt==2) cnt=0;
    printf("%lld\n",cnt/2);
    return 0;
}

8. CF1311E Construct the Binary Tree

题目大意:给定 n 和 d,你需要构造一棵 n 个点的二叉树,满足所有点的深度之和恰好为 d。2≤n,d≤5000。 

Solution:

首先考虑深度最小的情况,也就是完全二叉树。如果 d 小于这个数直接不可能。

否则,我们考虑对这棵树进行修改,每次让总深度 +1。

首先,随便拎出来一条最深的链,为了方便我们直接把 1 到 n 拎出来。

然后我们开始倒序考虑不在这条链上的所有点,每次尝试把这个点的深度 +1。

只需要让这个点的父节点变为链上深度等于它的节点即可。

每次我们刚好把总深度 +1,所以一定能够得到所有可行的结果。如果最后还没达到 d 那就不可能。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e3+5;
int t,n,d,fa[N],dep[N],a[N],mx,ans;
bool vis[N];
signed main(){
    scanf("%lld",&t);
    while(t--){
        scanf("%lld%lld",&n,&d);
        memset(vis,0,sizeof(vis)),mx=0,a[0]=1;
        for(int i=2;i<=n;i++)
            fa[i]=i/2,dep[i]=dep[fa[i]]+1,d-=dep[i],mx=max(mx,dep[i]);    //求出每个点的父亲和深度。mx:最大深度。 
        if(d<0){puts("NO");continue;}     //考虑深度最小的情况,也就是完全二叉树。如果 d 小于这个数直接不可能。 
        int x=n; 
        while(x) a[dep[x]]=x,vis[x]=1,x=fa[x];    //找出一条最深的链,标记在这条链上的点。 
        for(int i=n;i>=1;i--){    //倒序考虑不在这条链上的所有点。 
            if(vis[i]) continue;
            int pre=mx;
            while(dep[fa[i]]<pre&&d){
                fa[i]=a[dep[fa[i]]+1],dep[i]=dep[fa[i]]+1;    //每次尝试把这个点的深度 +1。
                if(dep[i]>mx) mx++,a[mx]=i,vis[i]=1;     //dep[i]>mx 相当于节点 i 接到了最深的链的后面。修改相关信息。 
                d--;
            }
        }
        if(d){puts("NO");continue;} 
        puts("YES");
        for(int i=2;i<=n;i++)
            printf("%lld%c",fa[i],i==n?'\n':' ');
    }
    return 0;
}

9. NOIP 2013 华容道

题目大意:有一个 n × m 的棋盘,其中有若干个位置是障碍,还有一个空白位置,其它位置都是棋子。有 q 组询问,每次告诉你一个指定的棋子 (SX,SY),空白位置 (EX,EY) 和目标位置 (TX,TY),你需要求出将指定的棋子移动到目标位置最少需要移动几次。1≤n,m≤30,1≤q≤500。

Solution:

首先我们有一个暴力的想法。设 f(i,j,k,l) 表示指定棋子位于 (i,j),空格位于 (k,l) 的情况下,到达目标点的最短路径。

建图直接看下一步往哪走。时间复杂度 O(n2m2q),难以通过本题。

注意到,空格无论怎样,一定要移动到指定棋子周围才能起作用。

所以直接设 f(i,j,k) 表示指定棋子位于 (i,j),空格位于指定棋子的上下左右哪个方向。

首先需要预处理状态之间的转移。跑一遍 bfs 即可。

总时间复杂度 O(n2m2+nmq log(nm))。

10. CF875F Royal Questions

题目大意:有 n 个王子 m 个公主,每一个公主喜欢其中两个王子,还有一个美丽度。你需要将王子和公主配对,使得每一个公主都和自己喜欢的王子配对,并且配对的公主美丽度之和最大。n,m≤2×105

Solution:

每一个公主可以抽象为一条边。

一个王子和一个公主配对,我们会发现答案形成了若干个基环树。(每一个连通块点数等于边数,刚好可以配对)

实际上就是要求整张图的最大生成环套树。

先将所有边排序。接下来对于每一条边 (u,v):

  • 若 u,v 不连通且分别位于两棵树内:加入这条边,得到一棵新的树。
  • 若 u,v 不连通且分别位于一棵树和一棵基环树内:加入这条边,得到一个基环树。
  • 若 u,v 不连通且分别位于两棵基环树内:无法加入。
  • 若 u,v 连通且位于一棵树内:加入这条边,树变成基环树。
  • 若 u,v 连通且位于一棵基环树内:无法加入。

只需要在并查集的基础上记录一个 flag 数组表示是否是基环树即可。

时间复杂度 O(m log m)。

11. BZOJ 2238 Mst

题目大意:一张图,q 次询问删掉某条边之后最小生成树的大小。询问相互独立。n≤50000,m,q≤105。 

Solution:

首先随便找一棵最小生成树。

如果删掉的边不在生成树上那么无影响。

删掉树边等价于求跨过这条边的非树边的最小权值。

如果用树剖的话,时间复杂度是两个 log 的。

我们可以把其它所有的边按照边权从小到大排序。

接下来相当于每次要把未被标记的边打上标记。

用并查集维护某个点上方第一个未被标记的点,这样一来每一条边只会被访问一次。

时间复杂度 O(mα(n))。

posted @ 2020-07-27 10:41  maoyiting  阅读(632)  评论(1编辑  收藏  举报