week5-基础图论

本文介绍了三个基于图论的算法问题:1) 计算两个节点间的危险系数,涉及关键点搜索;2) 在大规模有向图中寻找每个节点能到达的最大节点;3) 最小河蟹封锁策略,解决无冲突封锁所有路径问题。通过深度优先搜索和反向图遍历等方法解决这些问题,展示了图算法在实际问题中的应用。
摘要由CSDN通过智能技术生成

1.[蓝桥杯 2013 国 C] 危险系数

题目背景

抗日战争时期,冀中平原的地道战曾发挥重要作用。

题目描述

地道的多个站点间有通道连接,形成了庞大的网络。但也有隐患,当敌人发现了某个站点后,其它站点间可能因此会失去联系。

我们来定义一个危险系数 DF(x,y):

对于两个站点 x 和 y(x!=y), 如果能找到一个站点 z,当 z被敌人破坏后,x 和 y 不连通,那么我们称 z 为关于 x,y 的关键点。相应的,对于任意一对站点 x 和 y,危险系数 DF(x,y)就表示为这两点之间的关键点个数。

本题的任务是:已知网络结构,求两站点之间的危险系数。

输入格式

输入数据第一行包含 2 个整数 n(2<= n <= 1000),m(0 <= m <= 2000),分别代表站点数,通道数。

接下来 m 行,每行两个整数 u,v(1 <=u,v <=n,u!=v) 代表一条通道。

最后 1 行,两个数 u,v,代表询问两点之间的危险系数 DF(u,v)。

输出格式

一个整数,如果询问的两点不连通则输出 -1。

样例 #1

样例输入 #1

7 6
1 3
2 3
3 4
3 5
4 5
5 6
1 6

样例输出 #1

2

提示

时限 1 秒, 64M。蓝桥杯 2013 年第四届国赛

这道题的思路非常容易想到,首先我们需要先存下这个图。这里的数据规模不算太大,所以我们既可以用二维数组存图,也可以用vector数组来存图。这里因为要熟悉vector存图的规则,所以我使用vector来存图,然后从开始的节点进行深度优先搜索(dfs),这里要注意:这里一定要使用回溯法来进行搜索,应为从起点到终点可能不只有一条路径。在每次到达终点后我们都记录到达终点的路线的数量,并记录路径上的每个点的经过次数。如果某个点的经过次数等于路线的数量,就代表每条路径都需要用到这个点,这就是个关键点。思路就是这么简单。

完整代码如下:

#include<iostream>
#include<vector>
using namespace std;    
#define N 1005
int n,m,st,ed,num[N],ret;
bool book[N];  //标记数组
vector<int>g[N];  //用于存图
void dfs(int now){  //now代表当前节点
    int a; 
    if(now==ed){  
        ret++;  //如果到达终点,路线数++
        for(int i=1;i<=n;i++){
            if(book[i]){
                num[i]++;  //把经过的点都++
            }
        }
        return;  //一定要记得回溯
    }
    for(int i=0;i<g[now].size();i++){
        a=g[now][i];  
        if(!book[a]){
            book[a]=true;  //如果没有经过,就标记它,并且对它进行搜索
            dfs(a);
            book[a]=false;  //回溯时要取消标记
        }
    }
}
int main(){ 
    cin>>n>>m;
    int x,y,ans=0;
    for(int i=0;i<m;i++){
        cin>>x>>y;
        g[x].push_back(y);
        g[y].push_back(x);  //因为是无向图,所以要存两次
    }  
    cin>>st>>ed;
    book[st]=true;  //注意,在开始搜索之前,要把起点标记为已经使用的情况
    dfs(st);  //开始搜索
    for(int i=1;i<=n;i++){
        if(num[i]==ret){
            ans++;  //如果某节点的经过次数等于路线总数,代表它是关键点
        }
    }
    cout<<ans-2;  //注意要把起点和终点去除
    return 0;
}

2.图的遍历

题目描述

给出 N 个点,M 条边的有向图,对于每个点 v,求 A(v) 表示从点 v 出发,能到达的编号最大的点。

输入格式

第 1 行 2 个整数 N,M,表示点数和边数。

接下来 M 行,每行 2 个整数 U_i,V_i,表示边 (U_i,V_i)。点用 1,2…N 编号。

输出格式

一行 N 个整数 A(1),A(2),…,A(N)。

样例 #1

样例输入 #1

4 3
1 2
2 4
4 3

样例输出 #1

4 4 3 4

提示

  • 对于 60% 的数据,1 <= N,M <=10^3。
  • 对于 100% 的数据,1 <= N,M <=10^5。

这道题一看上去最容易想到的方法就是对每个点进行深搜,然后在每次访问新节点的时候更新最大值。我一开始也是这么想的,但是当我写出代码,满怀信心的交上去的时候,它直接给我报了TLE,这就引起了我的深思。显然,这里的数据规模达到了惊人的10^5,全部走一遍能的方法显然是行不通的。所以,我又苦思冥想许久(好像也没有多久),终于想到了反图!(好像是直接去看了题解才知道有这个东西)我们不妨反过来思考一下:要我们找每个点能到达的最大的节点,我们其实可以想,最大的节点可以到达哪里。然后我们就可以把从最大的节点能够访问的节点的结果都设为这个最大的节点的编号。从最大的点开始,每个节点只访问一遍即可。

这里我画几个图来理解一下:(灵魂画手上线

在这里插入图片描述

这个是题目中正常的图,现在要存反图的话就会变成下面这样子:
在这里插入图片描述

箭头方向发生了相反的改变,所以我们要从最大的点开始遍历这个图,并将最大的点能访问到的点的结果设置为这个最大节点的编号,如下所示:
在这里插入图片描述

因为每个节点只被访问一次,所以3之后的节点就不会再被访问,所以3的结果也就是本身3.

下面是完整代码实现:

#include<iostream>
#include<vector>
using namespace std;    
#define N 100005
vector<int>g[N];  //存图
bool book[N];  //标记数组
int ans[N];  //保存每个节点的结果
void dfs(int now,int ret){  //now表示当且节点的编号,ret表示最大节点的编号
    ans[now]=ret;  //把最大节点能访问到的节点的结果都变为最大节点的编号
    book[now]=1;  //标记该节点
    int x;
    for(int i=0;i<g[now].size();i++){  //遍历每一条边
        x=g[now][i];
        if(!book[x]){
            book[x]=1;
            dfs(x,ret);  //无需回溯
        }
    }
}
int main(){
    int n,m,x,y;
    cin>>n>>m;
    for(int i=0;i<m;i++){
        cin>>x>>y;
        g[y].push_back(x);  //反向存图
    }
    for(int i=n;i>=1;i--){
        if(!book[i]){
            dfs(i,i);            
        }
    }
    for(int i=1;i<=n;i++){
        cout<<ans[i]<<" ";  //输出每个结果
    }
    return 0;
}

3.封锁阳光大学

题目描述

曹是一只爱刷街的老曹,暑假期间,他每天都欢快地在阳光大学的校园里刷街。河蟹看到欢快的曹,感到不爽。河蟹决定封锁阳光大学,不让曹刷街。

阳光大学的校园是一张由 n 个点构成的无向图,n 个点之间由 m条道路连接。每只河蟹可以对一个点进行封锁,当某个点被封锁后,与这个点相连的道路就被封锁了,曹就无法在这些道路上刷街了。非常悲剧的一点是,河蟹是一种不和谐的生物,当两只河蟹封锁了相邻的两个点时,他们会发生冲突。

询问:最少需要多少只河蟹,可以封锁所有道路并且不发生冲突。

输入格式

第一行两个正整数,表示节点数和边数。
接下来 m行,每行两个整数 u,v,表示点 u 到点 v 之间有道路相连。

输出格式

仅一行如果河蟹无法封锁所有道路,则输出 Impossible,否则输出一个整数,表示最少需要多少只河蟹。

样例 #1

样例输入 #1

3 3
1 2
1 3
2 3

样例输出 #1

Impossible

样例 #2

样例输入 #2

3 2
1 2
2 3

样例输出 #2

1

提示

【数据规模】
对于 100% 的数据,1<=n<= 10^4,1<= m <= 10^5,保证没有重边。

这道题本蒟蒻是一开始真的一点想法都没有,当时在我思考良久之后,我就灵感迸发(其实就是看了题解,顿时茅舍顿开),这里就引用题解中这位大佬的解释来解释这道题的思路。

这道题最关键的是如何抽象题目中的语句:①每一条边所连接的点中,至少要有一个被选中。②每一条边所连接的两个点,不能被同时选中。由此,可以推断出:每一条边都有且仅有一个被他所连接的点被选中

那么这句话该怎么用代码实现呢?我们可以考虑用涂色法来实现这个思路。这里我来画个图来解释一下(面向画图的程序设计):

在这里插入图片描述

这里有5个节点,我们将相邻的节点都进行涂色,但要注意,相邻的点的颜色是不可以一样的,否则就会出现河蟹(水晶先锋)打架的情况,一旦出现这种情况就是Impossible,反之就是可以正常封锁。即每种图只对应两种情况:impossible和正常封锁。

正常封锁可以是以下的情况:

在这里插入图片描述

在这里我们就可以去绿色节点和红色节点个数较少的那个数量为最终的答案,这里最终的答案也就是绿色节点个数:3.

所以在知道思路后,就可以很容易的写出这道题,完整注释代码如下:

#include<iostream>
#include<vector>
using namespace std;    
#define N 100005
vector<int>g[N];  //存图(不会题解大佬的链式向前星qwq)
int col[N],sum[2];  //col[i]为i节点的颜色,sum[2]代表两只颜色的节点数量
bool book[N];  //标记数组,这里每个节只访问一次
bool dfs(int now,int color){  //now为现在的节点编号,color为现在的颜色,1代表红,0代表绿
    bool ans;  
    int a;
    ans=true;  //初始化结果为true
    for(int i=0;i<g[now].size();i++){  //遍历每一条边
        a=g[now][i];  
        if(book[a]){  
            if(col[a]!=color){  //如果重复遍历,判断是否颜色冲突
                ans=false;
                return ans;  //如果冲突就代表河蟹(水晶先锋)要打架了,
            }
        }
        else{ 
            book[a]=1;  //标记这个节点为使用过
            col[a]=color;  //将这个节点涂色
            sum[color]++;//这种颜色的节点个数++  
            ans=ans&&dfs(a,1-color);  //继续深搜,用&&可以保证如果一个结果为false则最终结果为false
        }
    }
    return ans;
}
int main(){
    int n,m,x,y,ans=0;
    bool ret;
    cin>>n>>m;
    for(int i=0;i<m;i++){
        cin>>x>>y;
        g[x].push_back(y);
        g[y].push_back(x);  //因为是无向图,所以要存两次
    }
    for(int i=1;i<=n;i++){
        if(book[i]==0){  //如果没有搜过,就开搜
            sum[0]=0;
            sum[1]=0;  //初始化为0
            ret=dfs(i,1);  
            if(ret){
                ans+=min(sum[0],sum[1]);  //注意:这里要取最优解,所以要取min
            }
            else{
                cout<<"Impossible"; //如果不行就输出"Impossible"
                return 0;  //直接退出
            }                
        }
    }
    cout<<ans;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值