第18期:图论基础

1.用DFS求连通块

1.1 UVA572 油田 Oil Deposits

#include<bits/stdc++.h>
using namespace std;
const int maxn=100+5;

char pic[maxn][maxn];
int m,n,idx[maxn][maxn];

void dfs(int r,int c,int id){
	if(r<0 || r>=m || c<0 || c>=n) return;//“出界”的格子
	if(idx[r][c]>0 || pic[r][c]!='@') return;//不是“@”或者已经访问过的格子
	idx[r][c]=id;//连通分量编号
	for(int dr=-1;dr<=1;dr++){
		for(int dc=-1;dc<=1;dc++){
			if(dr!=0 || dc!=0){
				dfs(r+dr,c+dc,id);
			}
		}
	} 
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(NULL); cout.tie(NULL);
	while(cin>>m>>n&&m&&n){
		for(int i=0;i<m;i++) cin>>pic[i];
		memset(idx,0,sizeof(idx));
		int cnt=0;
		for(int i=0;i<m;i++){
			for(int j=0;j<n;j++){
				if(idx[i][j]==0&&pic[i][j]=='@') dfs(i,j,++cnt);
			}
		}
		cout<<cnt<<endl;
	}
	return 0;
} 

这道题目的算法为:种子填充(floodfill)。

1.2 古代象形符号(待补)

先将白色区域染色,只留下文字内部的白色区域,然后在扫描文字时计算一下扫到的内部白色块,安装文字对应的内部空白数保存好答案,然后排序输出。

#include <stdio.h>
#include <string.h>
#include <algorithm>
struct node{
    int x, y;
} arr[60010];
struct queue{
    int h, t;
    void pop(){ ++h; }
    void push(int x, int y){
        arr[t].x = x;
        arr[t].y = y;
        ++t;
    }
    bool empty(){ return h == t; }
} q;
int book[210][210];
char g2[210][60];
char g[210][210];
char hex[16][5];
char hieroglyphs[7] = "WAKJSD";
char sign[41000], len;
int dx[] = {-1, 0, 1, 0};
int dy[] = {0, 1, 0, -1};
int n, m;
void convert(int r, int c){//16进制位图转2进制
    for(int i = 0; i < r; i++){
        g[i + 1][0] = '0';
        int k = 1;
        for(int j = 0; j < c; j++){
            int k2;
            if(g2[i][j] < 'a') k2 = g2[i][j] - '0';
            else k2 = g2[i][j] - 'a' + 10;
            for(int r = 0; r < 4; r++)
                g[i + 1][k++] = hex[k2][r];
        }
        g[i + 1][k] = '0';
        g[i + 1][k + 1] = '\0';
    }
    for(int i = 0; i < m; i++)
        g[0][i] = g[n - 1][i] = '0';
    g[0][m] = g[n - 1][m] = '\0';
}
void init(){//预先储存哈希值
    char code[5];
    for(int i = 0; i < 16; i++){
        for(int j = 0; j < 4; j++)
            if(i & (1 << j))
                code[3 - j] = '1';
            else code[3 - j] = '0';
        code[5] = '\0';
        strcpy(hex[i], code);
    }
}
bool isValid(int x, int y){
    return x >= 0 && y >= 0 && x < n && y < m;
}
void floodfill_white(int x, int y, char c1, char c2){//队列BFS
    q.h = q.t = 0;
    q.push(x, y);
    memset(book, 0, sizeof(book));
    book[x][y] = 1;
    while(!q.empty()){
        int kx = arr[q.h].x;
        int ky = arr[q.h].y;
        q.pop();
        g[kx][ky] = c2;
        for(int i = 0; i < 4; i++){
            int nx = kx + dx[i];
            int ny = ky + dy[i];
            if(isValid(nx, ny) && g[nx][ny] == c1 && book[nx][ny] == 0)
                book[nx][ny] = 1, q.push(nx, ny);
        }
    }
}
int floodfill_black(int x, int y, char c1, char c2){//DFS扫描文字
    g[x][y] = c2;
    int ans = 0;
    for(int i = 0; i < 4; i++){
        int nx = x + dx[i];
        int ny = y + dy[i];
        if(isValid(nx, ny)){
            if(g[nx][ny] == '0')//某个像素点邻接有白色
                ++ans, floodfill_white(nx, ny, '0', 'w');
            else if(g[nx][ny] == '1')
                ans += floodfill_black(nx, ny, c1, c2);
        }
    }
    return ans;
}
int main(){
    init();
    int r, c, t = 1;
    while(scanf("%d%d", &r, &c), (r || c)){
        getchar();
        for(int i = 0; i < r; i++)
            gets(g2[i]);
        n = r + 2;
        m = c * 4 + 2;
        convert(r, c);//转换
        floodfill_white(0, 0, '0', 'w');//外部区域染色
        len = 0;
        for(int i = 0; i < n; i++)
            for(int j = 0; j < m; j++)
                if(g[i][j] == '1'){
                    int white_size = floodfill_black(i, j, '1', 'b');//扫描文字并得到内部白色块数量
                    sign[len++] = hieroglyphs[white_size];
                }
        std::sort(sign, sign + len);//排序结果
        sign[len] = '\0';
        printf("Case %d: %s\n", t++, sign);
    }
}

2.用BFS求最短路

2.1 UVA816 Abbott的复仇 Abbott's Revenge

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 10;
//三个元素分别代表当前的坐标和在这个坐标时的朝向。
int d[MAXN][MAXN][4];
//接收数据的函数。
//返回值是 bool 类型是因为有多组数据。
//当我们返回 false 时就意味着输入结束。
//类中定义了节点的坐标 (r, c) 以及它的朝向。
//同时写了副本构造器并且重载了节点间的等于号,方便进行同类型间的赋值。
class Node {
public:
    int r;
    int c;
    int dir;
    Node (int r = 0, int c = 0, int dir = 0) : r(r), c(c), dir(dir) {}
    Node& operator = (const Node &d) {
        this->r = d.r;
        this->c = d.c;
        this->dir = d.dir;
        return *this;
    }
};
const int dr[]={-1,0,1,0};
const int dc[]={0,1,0,-1};
//三个节点分别表示:
//当前状态的 situation,起点 startNode 和终点 finalNode。
Node situation, startNode, finalNode;
//四个元素分别代表当前坐标和朝向以及此朝向能行走的方向。
bool has_edge[MAXN][MAXN][4][4];
string maze;
//输入过程:将4个方向和3种“转弯方式”编号为0~3和0~2,并且提供相应的转换函数 
const char* dirs="NESW"; // 顺时针旋转
const char* turns="FLR";
inline int dir_id(char c){ return strchr(dirs,c)-dirs; }
inline int turns_id(char c){ return strchr(turns,c)-turns; }
bool input() {
    memset(has_edge, 0, sizeof(has_edge));
    char tmpdir;
    cin >> maze;
    //利用一个 maze 变量,既可以判断是否结束,又可以记录迷宫名。
    //正常情况下,判断到 END 后,便不再进行读写。
    if(maze == "END") return false;
    //输入开始和结束坐标
    cin >> startNode.r >> startNode.c >> tmpdir 
             >> finalNode.r >> finalNode.c;
    situation.dir = dir_id(tmpdir);
    //下面是由于防止解题过程中发现开始点和结束点一样。
    //此举可以防止在开始时就退出解题函数的循环。
    situation.r = startNode.r + dr[situation.dir];
    situation.c = startNode.c + dc[situation.dir];
    //读入各个边。
    int tmpr, tmpc;
    string tmps;
    while(cin >> tmpr, tmpr) {
        cin >> tmpc;
        while(cin >> tmps, tmps != "*") {
            for(int i = 1; i < tmps.length(); i++) {
            	//通过刚刚写的转换函数存储各个方向的通行性。
                has_edge[tmpr][tmpc][dir_id(tmps[0])][turns_id(tmps[i])] = true;
            }
        }
    }
    return true;
}
//建立一个三元数组。
//下标分别对应在处于某个坐标某个方向。
//元素内容为处于此状态下,最短路的上一个节点坐标。
Node p[MAXN][MAXN][4];
//接下来是“行走”函数,根据当前状态和转弯方式,计算出后继状态
//将相对方向转化为实际坐标。
Node walk(const Node &n, int turn) {
    int dir = n.dir;
    //看上面我们写的转化函数。
    //我们可以依照 FLR,算出转过身之后的朝向。
    //当我们直行,dir 不变。
    //当我们要直行的时候,turn = 0。
    //当我们要左转的时候,turn = 1,dir 需要逆时针旋转一下。
    if(turn == 1) dir = (dir + 3) % 4;
    //当我们要右转的时候,turn = 2,dir 需要顺时针旋转一下。
    if(turn == 2) dir = (dir + 1) % 4;
    //转过来之后,我们就相当于直行了。
    //因此依据我们 dir 的转换函数。
    //我们上面的 dr 和 dc 数组只要按照 NESW,也即向上右下左的移动,
    //即可实现我们的目的。
    return Node(n.r + dr[dir], n.c + dc[dir], dir);
}
//防止访问越界。
inline bool ifInside(int r, int c) {
    return r > 0 && c > 0 && r < MAXN && c < MAXN;
}

inline void printAns(Node &n) {
    //用一个 vector 数组从终点向起点遍历,记录路径。
    vector<Node> vecNodes;
    while(true) {
        vecNodes.push_back(n);
        //这就能解释为什么在起点走一步后的距离为 0。
        //发现走到起点后面一个点之后说明找到了,退出。
        if(!d[n.r][n.c][n.dir]) break;
        Node tmp = n;
        n = p[n.r][n.c][n.dir];
        //为啥要把遍历过的 p 清零?因为可能不止有一个迷宫。
        //其实不清零问题也不大。
        //但是为了保险一点,防止出现梦幻错误,清空不会有错的。
        p[tmp.r][tmp.c][tmp.dir] = Node();
    }
    //别忘了,这才找到起点后面一个点。我们把起点也加到数组中。
    vecNodes.push_back(startNode);
    //题目中给出的是每行 10 个坐标。如需求更改,在此修改即可。
    const int eachLineNums = 10;
    //cnt 代表当前输出的是第几个点。
    int cnt = 0;
    for(vector<Node>::iterator itVec = vecNodes.end() - 1; 
        itVec >= vecNodes.begin(); itVec--) {
        //如果不在行首,输出一个空格。
        if(cnt % eachLineNums) cout << " ";
        //否则就在行首,缩进两个空格。
        else cout << "  ";
        cout << "(" << (*itVec).r << "," << (*itVec).c << ")";
        //如果是在行尾,换行。
        if(!(++cnt % eachLineNums)) cout << endl;
    }
    //假如这一行还没打印满 10 个坐标,换个行。
    if(vecNodes.size() % 10) cout << endl;
    return;
}

inline void solve() {
    //不管究竟能不能找到路,相应迷宫的名字必定会被打印出来。
    cout << maze << std::endl;
    queue<Node> q;
    //可能有多组数据,因此每次解题前将最短路清空。
    memset(d, -1, sizeof(d));
    Node n = situation;
    //初始化,将初始点出去的点距离设为 0。
    //因为在绝大部分情况是入口和出口不在一起。
    //在这种情况下,出去之后就不能再回到入口。
    //如果在入口设置 0,那么很可能会陷入死循环。
    //具体原因可看后面代码。
    d[n.r][n.c][n.dir] = 0;
    q.push(n);
    while(!q.empty()) {
        n = q.front(); q.pop();
        //检测是否到终点。
        if(n.r == finalNode.r && n.c == finalNode.c) {
            printAns(n);
            return;
        }
        for(int i = 0; i < 3; i++) {
            //检测是否可以行走。
            if(has_edge[n.r][n.c][n.dir][i]) {
                Node v = walk(n, i);
                if(ifInside(v.r, v.c) && d[v.r][v.c][v.dir] == -1) {
                    //路径长度加 1。
                    d[v.r][v.c][v.dir] = d[n.r][n.c][n.dir] + 1;
                    //设置前面的节点,以便打印结果。
                    p[v.r][v.c][v.dir] = n;
                    q.push(v);
                }
            }
        }
    }
    //代表没有到过终点,因此输出没有解决方案。
    //需要注意缩进 2 个空格。
    cout << "  No Solution Possible" << endl;
    return;
}

int main(){
	while(input()){
		solve();
	}
	return 0;
} 

3.拓扑排序

3.1 UVA10305 给任务排序 Ordering Tasks

把每个任务看做一个节点,一条任务之间的关系看做一条有向边,即可发现这就是个拓扑排序的模板。

#include<bits/stdc++.h>
using namespace std;
int n,m,t;
vector<vector<int> > G;//图
vector<int> book, topo;//标记,结果

bool dfs(int u){
	book[u]=-1;//-1标记为该结点正在访问中
	for(int i=0;i<G[u].size();i++){ //访问该结点指向了什么结点 
		if(book[G[u][i]]==-1) return false;//如果该结点中有结点也正在访问中,说明是有向环 
		//else if (book[G[u][i]] == 0 && !dfs(G[u][i])) { return false; } //原紫书代码 不好理解 换成下面的
		else if (book[G[u][i]] == 0) { dfs(G[u][i]); } //如果该节点指向的节点没被访问过 则对其进行递归访问
	}
	book[u]=1; topo[--t]=u;//标记该节点访问完毕 并记录到结果中
	return true; 
} 

bool toposort(){
	t=n;
	book=vector<int>(n+1);
	topo=vector<int>(n+1);
	for(int u=1;u<=n;u++){
		if(book[u]==0)
		if(!dfs(u)) return false;
	}
	return true;
}

int main(){
	while(cin>>n>>m){
		if(n==0&&m==0) break;
		G=vector<vector<int> >(n+1);
		for(int i=0,a,b;i<m;i++){
			cin>>a>>b;
			G[a].push_back(b);
		}
		if(!toposort()) cout<<"-1";
		else for(int i=0;i<n;i++){
			cout<<topo[i]<<" ";
		}
		cout<<endl;
	}
}

该题主要是考遍历拓扑基础,在题解中有很通俗易懂图文并茂的题解了,所以我只说一下大概的思路

主要是寻找入度数为0的节点(既没人被其他节点指向的节点),去除该节点后,重新计算与该节点相关的节点的入度数,如果为0则继续去除

#include<bits/stdc++.h>
using namespace std;

int main(){
	int n,m;//结点数 边数
	vector<vector<int> > G;//图
	vector<int> in;//入度数
	
	while(cin>>n>>m){
		if(n==0&&m==0) break;
		G=vector<vector<int> >(n+1);//有n个点
		in=vector<int>(n+1);
		for(int i=1,a,b;i<=m;i++){
			cin>>a>>b;//生动形象 a>>b a指向b
			G[a].push_back(b);//添加该点指向了什么
			in[b]++;//该点入度数+1 
		}
		queue<int> q; int t;
		for(int i=1;i<=n;i++){
			if(in[i]==0){
				q.push(i);//如果入度数为0则添加 
			}
		} 
		while(!q.empty()){//bfs遍历 
			t=q.front(); q.pop();
			cout<<t<<" ";
			for(int i=0;i<G[t].size();i++){//寻找除去该点后还有那些入度数为0的点
				in[G[t][i]]--;
				if(in[G[t][i]]==0) q.push(G[t][i]);
			} 
		}
		cout<<endl;
	} 
	return 0;
}

4.欧拉回路

4.1 UVA10129 单词 Play on Words

1本质上就是欧拉回路的应用,刘汝佳的算法竞赛入门经典里有提到过,下面具体讲讲实现思路
用邻接矩阵存图,将一个单词的首尾字母转化为对应的数字作为图的节点,如输入acm,a->1, m->13,则图G[1][13]++;
2判断是否存在欧拉回路有两个条件,第一个是要判断图是否联通,由于可以重复输入单词,所以不太方便数节点数,于是开一个vis数组全部置false,输入节点时把该节点打上true,在dfs遍历时遍历一个就把vis数组置false,这样,在判断时,如果遍历所有字母节点出现了vis[i] = true的情况就说明没有联通。
3第二个条件就是最多只能有两个点的入度不能等于出度,而且必须是其中一个入度-出度=1(作为终点),另一个出度-入度=1(作为起点),这个很简单,看下面代码就知道了。

#include<bits/stdc++.h>
using namespace std;

const int maxn=30;

string w;
int n,start;
//G 邻接矩阵存图,in,out记录入度和出度
int G[maxn][maxn],in[maxn],out[maxn],vis[maxn];

void dfs(int u){
	vis[u]=0;
	for(int i=1;i<=26;i++){
		if(G[u][i]&&vis[i]){
			dfs(i);
		}
	}
} 

bool solve(){
	//_in,_out记录欧拉回路中起点和终点的数量
	int t,u,v,_in=0,_out=0;
	memset(vis,0,sizeof(vis)); 
	memset(G,0,sizeof(G));
	memset(in,0,sizeof(in));
	memset(out,0,sizeof(out));
	cin>>t;
	for(int i=0;i<t;i++){
		cin>>w;
		u=w[0]-'a'+1;
		v=w[w.length()-1]-'a'+1;
		G[u][v]++; G[v][u]++; out[u]++; in[v]++;
		vis[u]=vis[v]=1;
	}
	dfs(u);
	for(int i=1;i<=26;i++){
		if(vis[i]) return false;
		if(in[i]!=out[i]){
			if(in[i]-out[i]==1) _in++;
			else if(out[i]-in[i]==1) _out++;
			else return false;
		}
	}
	if((_in==1&&_out==1)||(_in==0&&_out==0)){
		return true;
	}
	return false;
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(NULL); cout.tie(NULL);
	cin>>n;
	while(n--){
		if(solve()) cout<<"Ordering is possible."<<endl;
		else cout<<"The door cannot be opened."<<endl;
	}
	return 0;
}

5. 习题

5.1 UVA1599 理想路径 Ideal Path

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=4e5+5;
const int inf=0x7fffffff;

struct Edge{
	int nxt,to,c;
}edge[maxn];
int n,m;
int head[maxn],vis[maxn],dis[maxn];
int cnt;
queue<int> q1,q2,q3;

void add(int u,int v,int c){
	edge[++cnt].nxt=head[u];
	head[u]=cnt;
	edge[cnt].to=v;
	edge[cnt].c=c;
}

void bfs1(){//逆向求最短距离
	queue<int> q;
	fill(vis,vis+n+1,0);
	int u,v;
	q.push(n);
	vis[n]=1;
	while(!q.empty()){
		u=q.front();
		q.pop();
		for(int i=head[u];i;i=edge[i].nxt){
			v=edge[i].to;
			if(vis[v]==0){
				dis[v]=dis[u]+1;
				q.push(v);
				vis[v]=1;
			}
		}
	} 
}

void bfs2(){
	int u,v,c,minc;
	bool first=true;
	fill(vis,vis+n+1,0);
	vis[1]=1;
	for(int i=head[1];i;i=edge[i].nxt){//节点1的所有邻接点
		v=edge[i].to;
		c=edge[i].c;
		if(dis[v]==dis[1]-1){//距离减1的邻接点
			q1.push(v);
			q2.push(c); 
		} 
	}
	while(!q1.empty()){
		minc=inf;
		while(!q1.empty()){
			v=q1.front();
			c=q2.front();
			q1.pop();
			q2.pop();
			if(c<minc){
				while(!q3.empty()){//发现更小的色号,情况队列
					q3.pop(); 
				}
				minc=c;
			}
			if(c==minc){
				q3.push(v);
			}
		}
		if(first) first=false;
		else cout<<" ";
		cout<<minc;
		
		while(!q3.empty()){//所有为最小色号的节点
			u=q3.front();
			q3.pop();
			if(vis[u]) continue;
			vis[u]=1;
			for(int i=head[u];i;i=edge[i].nxt){//扩展每一个节点的邻接点
				v=edge[i].to;
				c=edge[i].c;
				if(dis[v]==dis[u]-1){
					q1.push(v);
					q2.push(c);
				} 
			} 
		}
	}
}
int main(){
	while(scanf("%d%d",&n,&m)==2){
		//初始化
		cnt=0;
		fill(head,head+n+1,0);
		fill(dis,dis+n+1,0);
		
		for(int i=1;i<=m;i++){
			int x,y,c;
			scanf("%d%d%d",&x,&y,&c);
			add(x,y,c);
			add(y,x,c);
		} 
		bfs1();
		printf("%d\n",dis[1]);
		bfs2();
		printf("\n");
	}
	return 0;
}

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值