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;
}