图论题

牛客 导航系统

题面:

在这里插入图片描述

思路:

在这里插入图片描述

code:
//https://ac.nowcoder.com/acm/contest/3007/I
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxm=1e3+5;
struct Node{
    int a,b,c;
}e[maxm*maxm];
int g[maxm][maxm];
int d[maxm][maxm];
int pre[maxm];
int ffind(int x){
    return pre[x]==x?x:pre[x]=ffind(pre[x]);
}
bool cmp(Node a,Node b){
    return a.c<b.c;
}
signed main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cin>>g[i][j];
            if(i!=j)d[i][j]=1e17;
        }
    }
    int cnt=0;
    for(int i=1;i<=n;i++){
        for(int j=i+1;j<=n;j++){
            if(g[i][j]!=g[j][i]){
                cout<<"No"<<endl;
                return 0;
            }else{
                e[++cnt]={i,j,g[i][j]};
            }
        }
    }
    sort(e+1,e+1+cnt,cmp);
    for(int i=1;i<=n;i++){//init pre
        pre[i]=i;
    }
    vector<int>ans;
    for(int i=1;i<=cnt;i++){//kruskal
        int a=e[i].a;
        int b=e[i].b;
        int x=ffind(a);
        int y=ffind(b);
        if(x!=y){
            pre[x]=y;
            d[a][b]=d[b][a]=e[i].c;
            ans.push_back(e[i].c);
        }
    }
    for(int k=1;k<=n;k++){//floyd
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
            }
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(d[i][j]!=g[i][j]){
                cout<<"No"<<endl;
                return 0;
            }
        }
    }
    cout<<"Yes"<<endl;
    sort(ans.begin(),ans.end());
    for(int v:ans){
        cout<<v<<endl;
    }
    return 0;
}

bzoj1001: [BeiJing2006]狼抓兔子

题面:

在这里插入图片描述

思路:

这类最小成本堵路问题要想到最小割。
根据最大流最小割定理可知答案就是最大流。
注意到是无向边,因此不需要建立反向的0边权虚边。
建图跑dinic计算最大流即可。

总结:用最小花费将原集合划分为两个集合这样的问题,试着想最小割

code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=2e6+1000;
int head[maxm],nt[maxm<<2],to[maxm<<2],w[maxm<<2],cnt;
int d[maxm];
int n,m;
int st,ed;
queue<int>q;
void add(int x,int y,int z){
    cnt++;nt[cnt]=head[x];head[x]=cnt;to[cnt]=y;w[cnt]=z;
}
bool bfs(){
    while(!q.empty())q.pop();
    q.push(st);
    for(int i=st;i<=ed;i++){
        d[i]=0;
    }
    d[st]=1;
    while(!q.empty()){
        int x=q.front();
        q.pop();
        for(int i=head[x];i;i=nt[i]){
            int v=to[i];
            if(!d[v]&&w[i]){
                d[v]=d[x]+1;
                if(v==ed)return 1;
                q.push(v);
            }
        }
    }
    return 0;
}
int dfs(int x,int flow){
    if(x==ed)return flow;
    int res=flow;
    for(int i=head[x];i;i=nt[i]){
        int v=to[i];
        if(w[i]&&d[v]==d[x]+1){
            int k=dfs(v,min(res,w[i]));
            if(!k)d[v]=-1;
            res-=k;
            w[i]-=k;
            w[i^1]+=k;
        }
        if(!res)break;
    }
    return flow-res;
}
int dinic(){
    int maxflow=0;
    while(bfs()){
        maxflow+=dfs(st,2e9);
    }
    return maxflow;
}
int getid(int i,int j){
    return (i-1)*m+j;
}
void init(){
    cnt=1;
}
signed main(){
    init();
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){//横向边
        for(int j=1;j<m;j++){
            int a=getid(i,j);
            int b=getid(i,j+1);
            int c;
            scanf("%d",&c);
            add(a,b,c);
            add(b,a,c);
        }
    }
    for(int i=1;i<n;i++){//纵向边
        for(int j=1;j<=m;j++){
            int a=getid(i,j);
            int b=getid(i+1,j);
            int c;
            scanf("%d",&c);
            add(a,b,c);
            add(b,a,c);
        }
    }
    for(int i=1;i<n;i++){//斜向边
        for(int j=1;j<m;j++){
            int a=getid(i,j);
            int b=getid(i+1,j+1);
            int c;
            scanf("%d",&c);
            add(a,b,c);
            add(b,a,c);
        }
    }
    st=getid(1,1);
    ed=getid(n,m);
    int ans=dinic();
    printf("%d\n",ans);
    return 0;
}

CodeForces1278 D. Segment Tree

题意:

给n个线段(L,R)
保证线段的端点只出现一次
如果线段i和线段j相交,则在i和j之间建立一条边
问最后建出的图是否是一棵树
数据范围:n<=5e5

思路:

先按左端点从小到大排序,这样遍历的时候就能保证当前的l大于之前的所有L了,
然后只需要在之前的点中找到R在当前的(L,R)中的点,然后建边就行了
但是遍历之前的所有点的话是O(n)的,这个地方可以用set维护,set自带的二分能很快找到大于等于当前L的位置
set里面存的是右端点R,但是我们要的是编号,因为题目保证端点不重复,所以用map将R映射成编号就行了(pair也行)
然后暴力建图就行了
为什么可以暴力呢?
因为题目要求是一颗树,最多就建立n-1条边,超过n-1则不成立(其实就是中途如果出现环则不成立)
可以用并查集判环,在最后还要用并查集判断是否只有一个连通块。

总结:
这种有点像二维偏序,先按左端点排序降维,然后set+自带二分就能找到满足条件的元素了。

code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=5e5+5;
struct Node{
    int l,r;
}e[maxm];
map<int,int>mark;
int pre[maxm];
set<int>s;
int ffind(int x){
    return pre[x]==x?x:pre[x]=ffind(pre[x]);
}
bool cmp(Node a,Node b){
    return a.l<b.l;
}
signed main(){
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)pre[i]=i;
    for(int i=1;i<=n;i++){
        scanf("%d%d",&e[i].l,&e[i].r);
        mark[e[i].r]=i;//用右端点记录点的编号
    }
    sort(e+1,e+1+n,cmp);//按l从小到大排序
    for(int i=1;i<=n;i++){//当前l大于等于set里面的所有l
        auto it=s.lower_bound(e[i].l);//找set中r在当前l-r之间的
        for(;it!=s.end();it++){
            if(*it>e[i].r)break;
            int x=ffind(mark[e[i].r]);
            int y=ffind(mark[*it]);
            if(x==y){//如果形成环则不满足
                puts("NO");
                return 0;
            }else{
                pre[x]=y;
            }
        }
        s.insert(e[i].r);
    }
    int sum=0;
    for(int i=1;i<=n;i++){
        if(pre[i]==i)sum++;
    }
    if(sum!=1){
        puts("NO");
    }else{
        puts("YES");
    }
    return 0;
}

CodeForces1139 C. Edgy Trees

题意:

给一棵n个顶点的树,和一个整数k。
树的每条边有黑白两种颜色。
现在要你构造一个长度为k的序列a,满足:
沿着树上两点间最短路a(1)走到a(2),然后走到a(3),一直走到a(k)
其中a(i)可以重复

如果走的过程中至少走过一条黑边,那么就满足条件。
问有多少种这样的序列,答案对1e9+7取模

思路:

直接算好难算,考虑正难则反。
用序列总数量减去不经过白边的数量。

用并查集将白边连接的点合并为一个块,
遍历每个块,假设当前块有x个点,那么不经过黑色点的序列就有xk,因为每个位置有x种选择。
累加每个块的答案,用从数量nk减去即可。

code:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxm=2e5+5;
const int mod=1e9+7;
int pre[maxm];
int cnt[maxm];
int n,k;
int ppow(int a,int b,int mod){
	int ans=1;
	while(b){
		if(b&1)ans=ans*a%mod;
		a=a*a%mod;
		b>>=1;	
	}
	return ans;
}
int ffind(int x){
    return pre[x]==x?x:pre[x]=ffind(pre[x]);
}
signed main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++){
        pre[i]=i;
        cnt[i]=1;
    }
    for(int i=1;i<n;i++){
        int a,b,c;
        cin>>a>>b>>c;
        if(c==0){
            int x=ffind(a);
            int y=ffind(b);
            if(x!=y){
                pre[x]=y;
                cnt[y]+=cnt[x];
            }
        }
    }
    int ans=ppow(n,k,mod);
    for(int i=1;i<=n;i++){
        if(pre[i]==i){
            ans=(ans-ppow(cnt[i],k,mod)+mod)%mod;
        }
    }
    cout<<ans<<endl;
    return 0;
}

CodeForces1139 E. Maximize Mex

题意:

n个学生m个俱乐部,每个学生有能力值p(i)和报名的俱乐部c(i)
接下来的d天,每天教练会删掉一个学生k(i)
然后在每一个俱乐部里面选出一位俱乐部内的成员,
这天的团队能力值是选出的成员的能力值的MEX
问每天的团队能力值最大是多少。

思路:

学生和俱乐部是二分图,用学生的能力值p(i)和c(i)建边
从mex=0开始跑二分图匹配,一直跑到无法匹配位置,答案就是无法匹配的mex

如果每天都删边重建图,然后重新匹配,复杂度过大,
逆向思维,从后往前计算,就变成了每次添加一条边,
添加完边之后直接继续跑二分图匹配。不需要再从0开始,从后往前的答案是单调不降的

code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=5e3+5;
int head[maxm],nt[maxm],to[maxm],cnt;
int p[maxm],c[maxm];
int mark[maxm];
int now[maxm];
int ans[maxm];
int k[maxm];
int n,m;
int d;
void add(int x,int y){
    cnt++;nt[cnt]=head[x];head[x]=cnt;to[cnt]=y;
}
int dfs(int x){
    for(int i=head[x];i;i=nt[i]){
        int v=to[i];
        if(!mark[v]){
            mark[v]=1;
            if(now[v]==-1||dfs(now[v])){
                now[v]=x;
                return 1;
            }
        }
    }
    return 0;
}
signed main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>p[i];
    for(int i=1;i<=n;i++)cin>>c[i];
    cin>>d;
    for(int i=1;i<=d;i++){
        cin>>k[i];
        mark[k[i]]=1;
    }
    for(int i=1;i<=n;i++){
        if(!mark[i]){
            add(p[i],c[i]);
        }
    }
    memset(now,-1,sizeof now);
    int last=0;
    for(int i=d;i>=1;i--){
        while(1){
            memset(mark,0,sizeof mark);
            if(dfs(last)){
                last++;
            }else{
                break;
            }
        }
        ans[i]=last;
        add(p[k[i]],c[k[i]]);
    }
    for(int i=1;i<=d;i++){
        cout<<ans[i]<<endl;
    }
    return 0;
}

CodeForces1328 E. Tree Queries

题意:

给一颗n个顶点的数,其中顶点1为根
m次询问,每次询问给出k个点,问你:
是否存在一个点x,满足这k个点与1到x这条路径的距离小于等于1,即k在路径上或者k与路径的距离为1
如果存在输出yes,否则输出no

思路:

假设s是这k个点中的一个点,因为需要满足s到路径的距离小于等于1,那么s要么在路径上要么与路径只有一条边。
发现两种情况下x的父节点都一定在路径上。
因此可以把k个点全部替换为父节点,那么新的k个点一定在一条链上。
如何判断是否是一条链呢,将这k个新点按深度从小到大排序,深度小的点一定是深度大的点的祖先,

对于相邻的新点x和y,判断lca(x,y)是否等于x,如果某一次不满足,则不成立。
单次查询的复杂度为O(klogn)

ps:
判断祖先关系也可以使用dfs序。

code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=2e5+5;
vector<int>g[maxm];
int f[maxm][20],maxd;
int d[maxm];
int n,m;
void dfs(int x){
    for(int v:g[x]){
        if(!d[v]){
            d[v]=d[x]+1;
            f[v][0]=x;
            dfs(v);
        }
    }
}
int lca(int a,int b){
    if(d[a]<d[b])swap(a,b);
    for(int i=maxd;i>=0;i--){
        if(d[f[a][i]]>=d[b]){
            a=f[a][i];
        }
    }
    if(a==b)return a;
    for(int i=maxd;i>=0;i--){
        if(f[a][i]!=f[b][i]){
            a=f[a][i],b=f[b][i];
        }
    }
    return f[a][0];
}
bool cmp(int a,int b){
    return d[a]<d[b];
}
signed main(){
    scanf("%d%d",&n,&m);
    maxd=log(n)/log(2)+1;
    for(int i=1;i<n;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        g[a].push_back(b);
        g[b].push_back(a);
    }
    d[1]=1;
    f[1][0]=1;
    dfs(1);
    for(int j=1;j<=maxd;j++){
        for(int i=1;i<=n;i++){
            f[i][j]=f[f[i][j-1]][j-1];
        }
    }
    while(m--){
        int k;
        scanf("%d",&k);
        vector<int>temp;
        for(int i=0;i<k;i++){
            int x;
            scanf("%d",&x);
            temp.push_back(f[x][0]);
        }
        sort(temp.begin(),temp.end(),cmp);
        int ok=1;
        for(int i=1;i<k;i++){
            int x=temp[i-1],y=temp[i];
            if(lca(x,y)!=x){
				ok=0;
				break;
			}
        }
        if(ok)puts("YES");
        else puts("NO");
    }
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值