搜索
宽度优先遍历:dfs
模板代码:
queue<int> q; void dfs(){ st[1] = true; // 表示1号点已经被遍历过 q.push(1); while (q.size()){ int t = q.front(); q.pop(); for (int i=h[t];i!=-1;i=ne[i]){//拓展结点:四联通、八联通 int j=e[i]; if (!st[j]){ st[j]=true;//标记j q.push(j); } } }
Flood Fill 模型
池塘计数
农夫约翰有一片 N∗M的矩形土地。最近,由于降雨的原因,部分土地被水淹没了。现在用一个字符矩阵来表示他的土地。每个单元格内,如果包含雨水,则用”W”表示,如果不含雨水,则用”.”表示。现在,约翰想知道他的土地中形成了多少片池塘。 每组相连的积水单元格集合可以看作是一片池塘。每个单元格视为与其上、下、左、右、左上、右上、左下、右下八个邻近单元格相连。请你输出共有多少片池塘,即矩阵中共有多少片相连的”W”块。 数据范围 1≤N,M≤1000 输入样例: 10 12 W........WW. .WWW.....WWW ....WW...WW. .........WW. .........W.. ..W......W.. .W.W.....WW. W.W.W.....W. .W.W......W. ..W.......W. 输出样例: 3
const int N=1010,M=N*N; typedef pair<int,int>PII;/*坐标是两维*/ queue<PII>q; char f[N][N]; bool st[N][N]; int n,m; /*每次出队队首元素,扩展八联通图,将结点符合条件的加入队列,直到队列为空*/ void bfs(int x,int y){ q.push({x,y}); st[x][y]=true; while(q.size()){ PII t=q.front(); q.pop(); /*八联通:挖去中间*/ for(int i=t.first-1;i<=t.first+1;i++) for(int j=t.second-1;j<=t.second+1;j++){ if(i==t.first&&j==t.second) continue;/*注意*/ if(f[i][j]=='.'||st[i][j]) continue; if(i<=0 || i>n || j<=0 || j>m) continue; st[i][j]=true; q.push({i,j}); } } } int main(){ cin>>n>>m; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)cin>>f[i][j]; /*bfs():只要符合条件且未遍历过,就dfs*/ int cnt=0; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ if(!st[i][j]&&f[i][j]=='W'){ cnt++; bfs(i,j); } } cout<<cnt<<endl; }
城堡问题:变量迭代+二进制
1 2 3 4 5 6 7 ############################# 1 # | # | # | | # #####---#####---#---#####---# 2 # # | # # # # # #---#####---#####---#####---# 3 # | | # # # # # #---#########---#####---#---# 4 # # | | | | # # ############################# # = Wall | = No wall - = No wall 请你编写一个程序,计算城堡一共有多少房间,最大的房间有多大。城堡被分割成 m?n个方格区域,每个方格区域可以有0~4面墙。注意:墙体厚度忽略不计。 输入格式:第一行包含两个整数 m 和 n,分别表示城堡南北方向的长度和东西方向的长度。接下来 m 行,每行包含 n 个整数,每个整数都表示平面图对应位置 的方块的墙的特征。每个方块中墙的特征由数字 P 来描述,我们用1表示西墙,2表示北墙,4表示东墙,8表示南墙,P 为该方块包含墙的数字之和。 例如,如果一个方块的 P 为3,则 3 = 1 + 2,该方块包含西墙和北墙。城堡的内墙被计算两次,方块(1,1)的南墙同时也是方块(2,1)的北墙。 输入的数据保证城堡至少有两个房间。 输出格式:共两行,第一行输出房间总数,第二行输出最大房间的面积(方块数)。 数据范围:1≤m,n≤50,0≤P≤15
const int N=60; typedef pair<int ,int>PII; int g[N][N]; bool st[N][N]; queue <PII>q; int n,m; /*每个点就是一个一平方米的面积四面有或没有墙,若此方向没有墙,则将此方向扩展到队列中*/ int dfs(int dx,int dy){ int area=0; int sx[4]={0,-1,0,1},sy[4]={-1,0,1,0}; q.push({dx,dy}); st[dx][dy]=true; while(q.size()){ PII t=q.front(); area++; q.pop(); for(int i=0;i<4;i++){//四联通 int a=t.first+sx[i],b=t.second+sy[i]; if(a<=0||a>n||b<=0||b>m) continue; if(st[a][b]||g[t.first][t.second]>>i&1) continue;/*二进制求第k位的数:右移*/ st[a][b]=true; q.push({a,b}); } } return area; } int main(){ int cnt=0,area; /*dfs*/ for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ if(!st[i][j]){ area=max(area,dfs(i,j)); cnt++; } } cout<<cnt<<endl; cout<<area<<endl; }
山峰和山谷:原题
FGD小朋友特别喜欢爬山,在爬山的时候他就在研究山峰和山谷。为了能够对旅程有一个安排,他想知道山峰和山谷的数量。给定一个地图,为FGD想要旅行的区域, 地图被分为 n×n 的网格,每个格子 (i,j) 的高度 w(i,j)是给定的。 若两个格子有公共顶点,那么它们就是相邻的格子,我们定义一个格子的集合 S为山峰(山谷)当且仅当:S的所有格子都有相同的高度。S 的所有格子都连通。 对于s属于S与s相邻的 s′ 不属于 S,都有 ws>ws′(山峰),或者 ws< ws′(山谷)。 如果周围不存在相邻区域,则同时将其视为山峰和山谷。你的任务是,对于给定的地图,求出山峰和山谷的数量,如果所有格子都有相同的高度,那么整个地图即 是山峰,又是山谷。 输入格式:第一行包含一个正整数 nn,表示地图的大小。接下来一个 n×nn×n 的矩阵,表示地图上每个格子的高度 ww。 输出格式:共一行,包含两个整数,表示山峰和山谷的数量。 数据范围:1≤n≤10001≤n≤1000,0≤w≤109
const int N=1010; typedef pair<int ,int>PII; int g[N][N]; bool st[N][N]; queue <PII>q; int n; /*每个点就是一个一平方米的面积四面有或没有墙,若此方向没有墙,则将此方向扩展到队列中*/ void dfs(int dx,int dy,bool & has_highter,bool &has_lower){ q.push({dx,dy}); st[dx][dy]=true; while(q.size()) { PII t=q.front(); q.pop(); for(int i=t.first-1;i<=t.first+1;i++) for(int j=t.second-1;j<=t.second+1;j++){ if(i<=0||i>n||j<=0||j>n) continue; if(g[i][j]!=g[t.first][t.second])/*扩展结点高度不一样*/{ if(g[i][j]>g[t.first][t.second]) has_highter=true; else has_lower=true; }else if(!st[i][j]){/*若相等且没有遍历过,必须放在上一个if的后边因为有遍历过但高度不同*/ st[i][j]=true; q.push({i,j}); } } } } int main(){ int peak=0,vally=0; /*dfs*/ for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){ if(!st[i][j]){ bool has_lower=false,has_highter=false; dfs(i,j,has_highter,has_lower); if(!has_highter) peak++; if(!has_lower) vally++; } } cout<<peak<<" "<<vally<<endl; }
最短路模型
走迷宫:记录距离
给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。 请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。数据保证 (1,1) 处和 (n,m) 处的数字为 0,且一定至少存在一条通路。 输入格式:第一行包含两个整数 n 和 m;接下来 n 行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。 输出格式:输出一个整数,表示从左上角移动至右下角的最少移动次数。 数据范围:1≤n,m≤100
const int N=110; typedef pair<int,int >PII; int f[N][N],n,m,d[N][N]; /*广度优先遍历*/ int bfs(){ memset(d,-1,sizeof d); d[1][1]=0; queue<PII> q; q.push({1,1});/*将源点到源点的距离设为0*/ int dx[]={-1,0,1,0},dy[]={0,1,0,-1}; /*广度优先遍历:使用的是队列*/ while (q.size()) { auto t=q.front(); q.pop(); for(int i=0;i<4;i++){/*遍历四个方向*/ int x=t.first+dx[i],y=t.second+dy[i]; if(x>0&&x<=n&&y>0&&y<=m&&f[x][y]==0&&d[x][y]==-1){ d[x][y]=d[t.first][t.second]+1;/*距离加1*/ q.push({x,y});/*将扩展点加入队列*/ } } } return d[n][m];/*返回(n,m)到源点的距离*/ }
迷宫问题:输出路径
给定一个 n×nn×n 的二维数组,如下所示:int maze[5][5] = { 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, }; 它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线。 数据保证至少存在一条从左上角走到右下角的路径。 输入格式:第一行包含整数 n。接下来n行,每行包含n个整数 0 或 1,表示迷宫。 输出格式:输出从左上角到右下角的最短路线,如果答案不唯一,输出任意一条路径均可。按顺序,每行输出一个路径中经过的单元格的坐标,左上角坐标为 (0,0),右下角坐标为 (n-1,n-1)。 数据范围:0≤n≤1000
const int N=1010,M=N*N; typedef pair<int,int>PII; int g[N][N],n; PII pre[N][N];/*记录前驱坐标(被谁扩展)*/ queue<PII> q; bool st[N][N]; /*bfs遍历*/ void bfs(int sx,int sy){ int dx[]={-1,0,1,0},dy[]={0,1,0,-1}; q.push({sx,sy}); st[sx][sy]=true; while(q.size()){ PII t=q.front(); q.pop(); /*四联通*/ for(int i=0;i<4;i++){ int a=t.first+dx[i],b=t.second+dy[i]; if(a<=0||a>n||b<=0||b>n) continue; if(g[a][b]||st[a][b]) continue; q.push({a,b}); st[a][b]=true; pre[a][b]=t; } } } int main(){ /*bfs*/ bfs(n,n); PII end(1, 1);/*初始化end的值从终点(1,1)倒退(n,n)*/ while (true){/*输出路径*/ cout<<end.first-1<<" "<<end.second-1<<endl; if (end.first==n&&end.second==n) break; end = pre[end.first][end.second]; } }
武士风度的牛
农民 John 有很多牛,他想交易其中一头被 Don 称为 The Knight 的牛。这头牛有一个独一无二的超能力,在农场里像 Knight 一样地跳(就是我们熟悉的象棋中马的走法)。虽然这头神奇的牛不能跳到树上和石头上,但是它可以在牧场上随意跳,我们把牧场用一个 x,y 的坐标图来表示。这头神奇的牛像其它牛一样喜欢吃草,给你一张地图,上面标注了 The Knight 的开始位置,树、灌木、石头以及其它障碍的位置,除此之外还有一捆草。 现在你的任务是,确定 The Knight 要想吃到草,至少需要跳多少次。The Knight 的位置用 K 来标记,障碍的位置用 * 来标记,草的位置用 H 来标记。 这里有一个地图的例子: 11 | . . . . . . . . . . 10 | . . . . * . . . . . 9 | . . . . . . . . . . 8 | . . . * . * . . . . 7 | . . . . . . . * . . 6 | . . * . . * . . . H 5 | * . . . . . . . . . 4 | . . . * . . . * . . 3 | . K . . . . . . . . 2 | . . . * . . . . . * 1 | . . * . . . . * . . 0 ---------------------- 1 0 1 2 3 4 5 6 7 8 9 0 、注意: 数据保证一定有解。 输入格式:第 1 行: 两个数,表示农场的列数 C 和行数 R。第 2..R+1 行: 每行一个由 C 个字符组成的字符串,共同描绘出牧场地图。 输出格式:一个整数,表示跳跃的最小次数。 数据范围:1≤R,C≤150
const int N=160; typedef pair<int,int>PII; char g[N][N]; bool st[N][N]; int n,m,dist[N][N]; queue <PII>q; int bfs(PII begin) { int dx[]={-2,-1,1,2,2,1,-1,-2}; int dy[]={1,2,2,1,-1,-2,-2,-1}; q.push(begin); st[begin.first][begin.second]; while(q.size()){ auto t=q.front(); q.pop(); /*特殊变换*/ for(int i=0;i<8;i++) { int a=t.first+dx[i],b=t.second+dy[i]; if(a<=0||a>n||b<=0||b>m) continue; if(st[a][b]||g[a][b]=='*') continue; if(g[a][b]=='H') return dist[t.first][t.second]+1; dist[a][b]=dist[t.first][t.second]+1;/*记录距离:前驱距离+1*/ st[a][b]=true; q.push({a,b}); } } } int main(){ cin>>m>>n; PII begin,end; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ cin>>g[i][j]; if(g[i][j]=='K') begin={i,j}; } cout<<bfs(begin)<<endl; }
抓住那头牛
农夫知道一头牛的位置,想要抓住它。农夫和牛都位于数轴上,农夫起始位于点 N,牛位于点 K。农夫有两种移动方式: 从 X 移动到 X1 或 X+1,每次移动花费一分钟 从 X 移动到 2X,每次移动花费一分钟 假设牛没有意识到农夫的行动,站在原地不动。农夫最少要花多少时间才能抓住牛? 输入格式:共一行,包含两个整数N和K。 输出格式:输出一个整数,表示抓到牛所花费的最少时间。 数据范围:0≤N,K≤105
const int N=1e5+10; int dist[N],n,m; queue<int>q; bool st[N]; int dfs(){ q.push(n); while(q.size()){ auto t=q.front(); if(t==m) return dist[m]; q.pop(); if(t+1<N&&!st[t+1]){ dist[t+1]=dist[t]+1; q.push(t+1); st[t+1]=true; } if(t-1>=0&&!st[t-1]){ dist[t-1]=dist[t]+1; q.push(t-1); st[t-1]=true; } if(t*2<N&&!st[t*2]){ dist[2*t]=dist[t]+1; q.push(2*t); st[2*t]=true; } } } int main(){ cin>>n>>m; cout<<dfs()<<endl; }
多源BFS
矩阵距离
给定一个 N 行 M 列的 01 矩阵 A,A[i][j] 与 A[k][l] 之间的曼哈顿距离定义为:横纵坐标之和 输出一个 N 行 M 列的整数矩阵 B,其中:B[i][j]=min1≤x≤N,1≤y≤M,A[x][y]=1dist(A[i][j],A[x][y]) 输入格式:第一行两个整数 N,M。接下来一个 N 行 M 列的 01 矩阵,数字之间没有空格。 输出格式:一个 N 行 M 列的矩阵 B,相邻两个整数之间用一个空格隔开。 数据范围:1≤N,M≤1000
const int N=1010; typedef pair<int,int>PII; bool st[N][N]; char g[N][N]; int dist[N][N]; int n,m; queue<PII>q; void bfs(){ int dx[4]={-1,0,1,0}, dy[4]={0,1,0,-1}; for(int i=1;i<=n;i++)/*找到所有的源点加入队列*/ for(int j=1;j<=m;j++) if(g[i][j]=='1'){ q.push({i,j}); st[i][j]=true; } while(q.size()){/*常规遍历*/ auto t=q.front(); q.pop(); for(int i=0;i<4;i++){ int a=t.first+dx[i],b=t.second+dy[i]; if(a<=0||a>n||b<=0||b>m) continue; if(g[a][b]=='1'||st[a][b]) continue; dist[a][b]=dist[t.first][t.second]+1; st[a][b]=true; q.push({a,b}); } } } int main(){ bfs(); for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++) cout<<dist[i][j]<<" "; cout<<endl; } }
最小步数模型
八数码:需要回溯状态
在一个 3×3 的网格中,1~8 这 8 个数字和一个 x 恰好不重不漏地分布在这 3×3 的网格中。 例如: 1 2 3 x 4 6 7 5 8 在游戏过程中,可以把 x 与其上、下、左、右四个方向之一的数字交换(如果存在)。我们的目的是通过交换, 使得网格变为如下排列(称为正确排列): 1 2 3 4 5 6 7 8 x 现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。 输入格式:输入占一行,将 3×3 的初始网格描绘出来。
/*bfs()核心代码:将二维装换成一维,每个字符串就是一种状态*/ int bfs(string str){ string res="12345678x"; queue<string >q; map<string,int>d; int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}; d[str]=0;/*从起始字符串到当前字符串需要0步*/ q.push(str); while(q.size()){ auto t=q.front(); q.pop(); if(t==res) return d[t];/*到结果状态的输出*/ int distance =d[t]; int k=t.find('x');/*找到x在一维中的位置*/ int x=k/3,y=k%3;/*找到x在二维中的位置##*/ for(int i=0;i<4;i++){/*遍历四个方向*/ int a=x+dx[i],b=y+dy[i]; if(a>=0&&b>=0&&a<3&&b<3){ swap(t[a*3+b],t[k]);/*交换k和扩展字符*/ if (!d.count(t)){/*如果当前字符串未被遍历过*/ d[t] = distance + 1; q.push(t); } swap(t[a * 3 + b], t[k]);/*回溯状态##*/ } } }
魔版:不需要回溯状态
Rubik 先生在发明了风靡全球的魔方之后,又发明了它的二维版本——魔板。这是一张有 8 个大小相同的格子的魔板: 1 2 3 4 8 7 6 5 我们知道魔板的每一个方格都有一种颜色。这 8 种颜色用前 8 个正整数来表示。可以用颜色的序列来表示一种魔板状态,规定从魔板的左上角开始, 沿顺时针方向依次取出整数,构成一个颜色序列。对于上图的魔板状态,我们用序列 (1,2,3,4,5,6,7,8) 来表示,这是基本状态。 这里提供三种基本操作,分别用大写字母 A,B,C 来表示(可以通过这些操作改变魔板的状态): A:交换上下两行; B:将最右边的一列插入到最左边; C:魔板中央对的4个数作顺时针旋转。 对于每种可能的状态,这三种基本操作都可以使用。你要编程计算用最少的基本操作完成基本状态到特殊状态的转换,输出基本操作序列。注意:数据保证一定有解。 输入格式:输入仅一行,包括 8 个整数,用空格分开,表示目标状态。 输出格式:输出文件的第一行包括一个整数,表示最短操作序列的长度。如果操作序列的长度大于0,则在第二行输出字典序最小的操作序列。 数据范围:输入数据中的所有数字均为 1 到 8 之间的整数。
char g[2][4]; map<string, pair<char,string> > pre;/*前驱:扩展到当前状态的前驱操作和状态*/ map<string,int> dist; queue<string>q; map<string,bool> st; string start="12345678"; void set1(string state){/*字符串转字符数组*/ for (int i = 0; i < 4; i ++ ) g[0][i] = state[i]; for (int i = 7, j = 0; j < 4; i --, j ++ ) g[1][j] = state[i]; } string get(){/*字符数组转字符串*/ string res; for (int i = 0; i < 4; i ++ ) res += g[0][i]; for (int i = 3; i >= 0; i -- ) res += g[1][i]; return res; } string move0(string state){/*操作1*/ set1(state); for (int i = 0; i < 4; i ++ ) swap(g[0][i], g[1][i]); return get(); } string move1(string state){/*操作2*/ set1(state); int v0 = g[0][3], v1 = g[1][3]; for (int i = 3; i > 0; i -- ) { g[0][i] = g[0][i - 1]; g[1][i] = g[1][i - 1]; } g[0][0] = v0, g[1][0] = v1; return get(); } string move2(string state){/*操作3*/ set1(state); int v = g[0][1]; g[0][1] = g[1][1]; g[1][1] = g[1][2]; g[1][2] = g[0][2]; g[0][2] = v; return get(); } /*dfs遍历:为什么从开始推结果不行*/ int dfs(string end) { if (start==end) return 0; q.push(start); st[start]=true; string state[3]; while(q.size()){ auto t=q.front(); q.pop(); state[0]=move0(t); state[1]=move1(t); state[2]=move2(t); for(int i=0;i<3;i++) { string temp=state[i]; if(!st[temp]){ st[temp]=true; dist[temp]=dist[t]+1; pre[temp]={i+'A',t};/*i+'A'对应操作;t:对应被扩展*/ q.push(temp); if(temp==end) return dist[end]; } } } } int main(){ string end; int temp; for(int i=1;i<=8;i++){ cin>>temp; end+=temp+'0'; } int step=dfs(end); string res; /*操作存储*/ while(end!=start){/*输出路径###*/ res+=pre[end].first; end=pre[end].second; } reverse(res.begin(), res.end());/*取反res*/ cout<<step<<endl; if (step > 0) cout << res << endl; }
双端队列广搜
电路维修
达达是来自异世界的魔女,她在漫无目的地四处漂流的时候,遇到了善良的少女翰翰,从而被收留在地球上。翰翰的家里有一辆飞行车。有一天飞行车的电路板突然出现了故障,导致无法启动。电路板的整体结构是一个 R 行 C 列的网格(R,C≤500) 每个格点都是电线的接点,每个格子都包含一个电子元件。 电子元件的主要部分是一个可旋转的、连接一条对角线上的两个接点的短电缆。 在旋转之后,它就可以连接另一条对角线的两个接点。电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置。 达发现因为某些元件的方向不小心发生了改变,电路板可能处于断路的状态。她准备通过计算,旋转最少数量的元件,使电源与发动装置通过若干条短缆相连。不过,电路的规模实在是太大了,达达并不擅长编程,希望你能够帮她解决这个问题。 注意:只能走斜向的线段,水平和竖直线段不能走。 输入格式:输入文件包含多组测试数据。第一行包含一个整数 T,表示测试数据的数目。对于每组测试数据,第一行包含正整数 R 和 C,表示电路板的行数和列数。之后 R 行,每行 C 个字符,字符是"/"和"\"中的一个,表示标准件的方向。 输出格式:对于每组测试数据,在单独的一行输出一个正整数,表示所需的最小旋转次数。如果无论怎样都不能使得电源和发动机之间连通,输出 NO SOLUTION。 数据范围:1≤R,C≤500,1≤T≤5
const int N=510; typedef pair<int,int>PII; int dist[N][N]; char g[N][N]; bool st[N][N]; int n,m; /*Dijkstra:迪杰斯特拉版的dfs*/ int bfs() { deque<PII>q;/*双端队列*/ int dx[4]={-1,-1,1,1}, dy[4]={-1, 1, 1, -1};/*点的四联通*/ int ix[4]={-1,-1,0,0}, iy[4]= {-1, 0, 0, -1};/*线路的四联通*/ char cs[] = "\\/\\/";/*扩展正确线路*/ memset(dist,0x3f,sizeof dist);/*多组输入*/ /*多次输入需要初始化*/ q.push_back({0,0}); memset(st, 0, sizeof st);/*多组输入*/ dist[0][0]=0; while(q.size()) { auto t=q.front();/*dsf队列是dist是单调的所以每次出队的都是当前dist的mi*/ q.pop_front(); if(st[t.first][t.second]) continue;/*防止一个点多次扩展*/ st[t.first][t.second]=true; if(t.first==n&&t.second==m) return dist[n][m]; 、 for(int i=0;i<4;i++) { int a=t.first+dx[i],b=t.second+dy[i];/*扩展点的坐标*/ if(a<0||a>n||b<0||b>m) continue; int ca = t.first + ix[i], cb = t.second + iy[i];/*扩展线路的坐标*/ int d=dist[t.first][t.second]+(g[ca][cb]!=cs[i]); if(d<dist[a][b]){/*是否需要更新*/ dist[a][b]=d; if (g[ca][cb] != cs[i]) q.push_back({a, b});/*0放队首1放队尾*/ else q.push_front({a, b}); } } } } int main(){ int T; cin>>T; while(T--){ cin>>n>>m; for(int i=0;i<n;i++) cin>>g[i]; if((n+m)&1) cout<<"NO SOLUTION"<<endl; else cout<<bfs()<<endl; }
双向搜索:
字符变换:如何实现双端的''同一函数''搜索
/*已知有两个字串 A, B 及一组字串变换的规则(至多 6 个规则):规则的含义为:在 A 中的子串 A1 可以变换为 B1、A2 可以变换为 B2…。 例如:A=abcd B=xyz变换规则为:abc → xu ud → y y → yz,则此时,A 可以经过一系列的变换变为 B,其变换的过程为: abcd → xud → xy → xyz共进行了三次变换,使得 A 变换为 B。 输入格式:输入格式如下: A B A1 B1 A2 B2 第一行是两个给定的字符串 A 和 B。接下来若干行,每行描述一组字串变换的规则。所有字符串长度的上限为 20。 输出格式:若在 10 步(包含 10 步)以内能将 A 变换为 B ,则输出最少的变换步数;否则输出 NO ANSWER!。
const int N=6; string a[N],b[N];/*起始规则;终止规则*/ string A,B;/*起始和终止*/ int n;/*规则数目*/ int extend(queue<string>&qc,map<string,int>&dc,map<string,int>&dist,string a[],string b[]){ /*1.扩展队列;2.扩展队列的dist; 3.另一队列dist; 4.变换条件字符串; 5.变化结果字符串*/ string t=qc.front(); qc.pop(); for(int i=0;i<t.size();i++)//枚举状态的的每个字符串 for(int j=0;j<n;j++){//枚举每个规则 if(t.substr(i,a[j].size() )==a[j]) {//从状态的第i的字符串到规则j长度的字符串和规则j字符串相等,则可以扩展 string temp=t.substr(0,i)+b[j]+t.substr(i+a[j].size());//扩展之后的字符串 if(dist.count(temp)) return dc[t]+1+dist[temp];//在另一边已经扩展过 if(dc.count(temp)) continue;//在当前队列已经扩展过 qc.push(temp); dc[temp]=dc[t]+1; } } return 11; } int bfs(){ if(A==B) return 0; map<string,int >da,db;/*da:起始距离;db:终点距离*/ queue<string>qa,qb;/*qa:起始队列;qb:终止队列*/ qa.push(A); qb.push(B); da[A]=da[B]=0; int t=0; while(qa.size()&&qb.size()){/*必须两个都存在若一边结束而另一边未结束且没有相与即无解*/ if(qa.size()<qb.size()) t=extend(qa,da,db,a,b);/*每次选择数目较小的扩展*/ else t=extend(qb,db,da,b,a); if(t<=10) return t;/*只要在另一队列中匹配不到就返回11,直到在另一队列找到才会返回真正的t*/ } return 11; } int main(){ cin>>A>>B; while(cin>>a[n]>>b[n]) n++; int step=bfs(); if(step>10) puts("NO ANSWER!"); else cout<<step<<endl; }
A*:利用启发函数缩小搜索范围
八数码
/*八数码:A-stars*/ #include<bits/stdc++.h> using namespace std; typedef pair<int,string>PIS; #define x first #define y second map<string,pair<string,char>>pre;//记录前驱及操作 map<string,int>dist; priority_queue< PIS,vector<PIS>, greater<PIS>> heap; /*小根堆,将元素的估计终点距离从小到大排序,第一维是估计距离,第二维是状态*/ int calculation(string state){//A-star:估价函数 int res=0; for(int i=0;i<9;i++) if(state[i]!='x'){ int t=state[i]-'1';//对应下标 res+=abs(i/3-t/3)+abs(i%3-t%3);//曼哈顿距离 } return res; } string bfs(string start) { int dx[4]={-1,1,0,0};int dy[4]={0,0,-1,1}; string end="12345678x"; char op[]="udlr";//操作 dist[start]=0; heap.push( {0+calculation(start),start} );//起点到t的距离+t估计到终点距离 while(heap.size()) { auto t=heap.top(); heap.pop(); string state=t.y; if(state==end) break; int x,y; for(int i=0;i<9;i++)/*找到x在3x3矩阵下的坐标*/ if(state[i]=='x'){ x=i/3,y=i%3; break; } string source =state;//暂时保存当前字符串状态 for(int j=0;j<4;j++){//四宫格扩展 int a=x+dx[j],b=y+dy[j]; if(a<0||a>=3||b<0||b>=3) continue; swap(state[a*3+y],state[x*3+b]);//交换位置:将在数组中位置转换成字符串中位置 if(!dist.count(state)||dist[state]>dist[source]+1){/*未扩展过或距离变小*/ dist[state]=dist[source]+1;//更新距离 heap.push({dist[state]+calculation(state),state}); pre[state]={source,op[j]};//存储前驱,扩展操作; } state=source; //要扩展四个方向,需要还原 } } //从终点回溯到起点 string res; while(end!=start){ res+=pre[end].y; end =pre[end].x; } reverse(res.begin(),res.end());//数组倒置 return res; } int main() { string start,seq; char c; while(cin>>c){ start+=c; if(c!='x') seq+=c; } int cent=0; //求逆序对 :若i<j,且a[i]>a[j],则称(a[i],a[j])是一对逆序对 for(int i=0;i<8;i++) for(int j=i+1;j<8;j++) if(seq[i]>seq[j]) cent++; if(cent%2) puts("unsolvable"); else cout<<bfs(start)<<endl; return 0; }
第k最短路
给定一张 N 个点(编号 1,2…N),M 条边的有向图,求从起点 S 到终点 T 的第 K 短路的长度,路径允许重复经过点或边。注意: 每条最短路中至少要包含一条边。 输入格式:第一行包含两个整数 N 和 M。接下来 M 行,每行包含三个整数 A,B 和 L,表示点 A 与点 B 之间存在有向边,且边长为 L。 最后一行包含三个整数 S,T 和 K,分别表示起点 S,终点 T 和第 K 短路。 输出格式:输出占一行,包含一个整数,表示第 K 短路的长度,如果第 K 短路不存在,则输出 ?1。 数据范围:1≤S,T≤N≤1000,0≤M≤104,1≤K≤1000,1≤L≤100
const int N=1000,M=200010; #define x first #define y second typedef pair<int,int>PII; typedef pair<int,PII>PIII; int n,m,S,T,k;//点数、边数、起点、终点 int h[N],rh[N],e[M],w[M],ne[M],idx;//正向邻接表的表头,反向邻接表的表头,边权 int dist[N],cnt[N]; int st[N]; int K; //邻接表创建 void add(int h[],int a,int b,int c){ e[idx]=b,w[idx]=c;ne[idx]=h[a],h[a]=idx++; //画图 } //dijkstra():估价函数,从终点到其他点的最小距离 void dijkstra(){ priority_queue<PII,vector<PII>,greater<PII>>heap; heap.push({0,T}); memset(dist,0x3f,sizeof dist); dist[T]=0; while (heap.size()){ auto t=heap.top(); heap.pop(); int ver =t.y; if(st[ver]) continue; for(int i=rh[ver];~i;i=ne[i]){/*从反边开始扩展*/ int j=e[i]; if(dist[j]>dist[ver]+w[i]) { dist[j]=dist[ver]+w[i]; heap.push({dist[j],j}); } } } } int astar(){ priority_queue<PIII,vector<PIII>,greater<PIII>> heap; heap.push({dist[S],{0,S}});/*估价值,真实值+编号*/ int cent;//遍历了几次 while(heap.size()) { auto t=heap.top(); heap.pop(); int ver=t.y.y,distance=t.y.x; cnt[ver]++; if(cnt[T]==K) return distance; for(int i=h[ver];~i;i=ne[i])//从正向图扩展,把能找到的边全部加入来,不管大小 { int j=e[i]; if (cnt[j] < K) heap.push({distance+w[i]+dist[j],{distance+w[i],j} }); } } return -1; } int main() { scanf("%d%d",&n,&m); memset(h,-1,sizeof h);//初始化表头; memset(rh,-1,sizeof rh); for(int i=0;i<m;i++){ int a,b,c; scanf("%d%d%d",&a,&b,&c);//起点、终点、边权 add(h,a,b,c);//正向邻接表 add(rh,b,a,c); //反向邻接表 } scanf("%d%d%d",&S,&T,&K); if(S==T) K++; dijkstra(); printf("%d\n",astar());
深度优先遍历
连通性模型:
迷宫:原题
一天Extense在森林里探险的时候不小心走入了一个迷宫,迷宫可以看成是由 n?n 的格点组成,每个格点只有2种状态,.和#,前者表示可以通行后者表示不能通行。 同时当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。 如果起点或者终点有一个不能通行(为#),则看成无法办到。 注意:A、B不一定是两个不同的点。 输入格式:第1行是测试数据的组数 k,后面跟着 k 组输入。每组测试数据的第1行是一个正整数 n,表示迷宫的规模是 n?n 的。接下来是一个 n?n 的矩阵,矩阵 中的元素为.或者#。再接下来一行是 4 个整数 ha,la,hb,lb,描述 A 处在第 ha 行, 第 la 列,B 处在第 hb 行, 第 lb 列。 注意到 ha,la,hb,lb 全部是从 0 开始计数的。 输出格式:k行,每行输出对应一个输入。能办到则输出“YES”,否则输出“NO”。 数据范围:1≤n≤100
const int N=110; char g[N][N]; bool st[N][N]; int ha,la,hb,lb,n ; int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1}; bool dfs(int x,int y){ if(g[x][y]=='#')return false;/*结束当前分支的递归*/ if(x==hb&&y==lb) return true; st[x][y]=true; for(int i=0;i<4;i++)/*四联通*/ { int a=x+dx[i],b=y+dy[i]; if(a<0||a>=n||b<0||b>=n) continue; if(st[a][b]) continue; // if(g[a][b]=='#') continue; if(dfs(a,b)) return true; } return false; } int main(){ int k; cin>>k; while(k--){ cin>>n; memset(st,0,sizeof st); for(int i=0;i<n;i++) for(int j=0;j<n;j++) cin>>g[i][j]; cin>>ha>>la>>hb>>lb; if(dfs(ha,la)) cout<<"YES"<<endl; else cout<<"NO"<<endl; } }
红与黑:树深度
有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。你站在其中一块黑色的瓷砖上,只能向相邻(上下左右四个方向)的黑色瓷砖移动。 请写一个程序,计算你总共能够到达多少块黑色的瓷砖。 输入格式输入包括多个数据集合。每个数据集合的第一行是两个整数 W 和 H,分别表示 x 方向和 y 方向瓷砖的数量。在接下来的 H 行中,每行包括 W 个字符。 每个字符表示一块瓷砖的颜色,规则如下 1)‘.’:黑色的瓷砖; 2)‘#’:红色的瓷砖; 3)‘@’:黑色的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一次。当在一行中读入的是两个零时,表示输入结束。 输出格式:对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。 数据范围:1≤W,H≤20
const int N=30; char g[N][N]; bool st[N][N]; int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1}; int n,m; int dfs(int x,int y){/*返回子树的深度*/ int cnt=1;/*子树根就一行*/ st[x][y]=true; for(int i=0;i<4;i++){ int a=x+dx[i],b=y+dy[i]; if(a<0||a>=n||b<0||b>=m) continue; if(g[a][b]!='.') continue; if(st[a][b]) continue; cnt+=dfs(a,b); } return cnt; } int main(){ while(cin>>m>>n,m||n) { memset(st,0,sizeof st); int x,y; for(int i=0;i<n;i++) for(int j=0;j<m;j++) { cin>>g[i][j]; if(g[i][j]=='@') x=i,y=j; } cout<<dfs(x,y)<<endl; } return 0; }
搜索顺序
马走日
马在中国象棋以日字形规则移动。请编写一段程序,给定 n?m 大小的棋盘,以及马的初始位置 (x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少 途径遍历棋盘上的所有点。 输入格式:第一行为整数 T,表示测试数据组数。每一组测试数据包含一行,为四个整数,分别为棋盘的大小以及初始位置坐标 n,m,x,y。 输出格式:每组测试数据包含一行,为一个整数,表示马能遍历棋盘的途径总数,若无法遍历棋盘上的所有点则输出 0。 数据范围:1≤T≤9,
const int N=10; int n,m; bool st[N][N]; int ans;/*全局变量*/ int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2}; int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1}; void dfs(int x,int y,int cnt){ if(cnt==n*m){/*cnt:点数*/ ans++; return; } st[x][y]=true; for(int i=0;i<8;i++){ int a=x+dx[i],b=y+dy[i]; if(a<0||a>=n||b<0||b>=m) continue; if(st[a][b]) continue; dfs(a,b,cnt+1); } st[x][y]=false; } int main(){ int T; cin>>T; while(T--){ int x,y; cin>>n>>m>>x>>y; memset(st,0,sizeof st); ans=0; dfs(x,y,1); cout<<ans<<endl; } return 0; }
单词接龙
单词接龙是一个与我们经常玩的成语接龙相类似的游戏。现在我们已知一组单词,且给定一个开头的字母,要求出以这个字母开头的最长的“龙”,每个单词最多 被使用两次。在两个单词相连时,其重合部分合为一部分,例如 beast 和 astonish ,如果接成一条龙则变为 beastonish。 我们可以任意选择重合部分的长度,但其长度必须大于等于1,且严格小于两个串的长度,例如 at 和 atide 间不能相连。 输入格式:输入的第一行为一个单独的整数 nn 表示单词数,以下 nn 行每行有一个单词(只含有大写或小写字母,长度不超过20),输入的最后一行为一个单个字符,表示“龙”开头的字母。 你可以假定以此字母开头的“龙”一定存在。 输出格式:只需输出以此字母开头的最长的“龙”的长度。 数据范围:n≤20
#include<bits/stdc++.h> using namespace std; const int N=30; string word[N]; int used[N],g[N][N]; int n,ans;/*ans全局变量爆搜*/ void dfs(string dragon,int last)/*last:龙最后字母*/ { ans=max((int)dragon.size(),ans); used[last]++; for(int i=0;i<n;i++){ if(g[last][i]&&used[i]<2) dfs(dragon + word[i].substr(g[last][i]), i); } used[last]--; } int main(){ cin>>n; for(int i=0;i<n;i++) cin>>word[i]; char start; cin>>start; /*字符串i的后缀和字符串j的前缀最小重合数:从i的末尾,j的开头开始*/ for(int i=0;i<n;i++) for(int j=0;j<n;j++) for(int k=1;k<min(word[i].size(),word[j].size()) ;k++) if(word[i].substr(word[i].size()-k,k)==word[j].substr(0,k)){ g[i][j]=k; break; } /*遍历所有的龙头*/ for(int i=0;i<n;i++) if(word[i][0]==start){ dfs(word[i],i); } cout<<ans<<endl; return 0; }
分成质数组
给定 n个正整数,将它们分组,使得每组中任意两个数互质。至少要分成多少个组? 输入格式:第一行是一个正整数 n第二行是 n个不大于10000的正整数。 输出格式:一个正整数,即最少需要的组数。 数据范围:1≤n≤10
#include<bits/stdc++.h> using namespace std; const int N=10; int n; int group[N][N],a[N]; int ans; bool st[N]; int gcd(int a,int b){ return b?gcd(b,a%b):a; } bool check(int g[],int gc,int num){ for(int i=0;i<gc;i++){ if(gcd(g[i],a[num])>1) return false; } return true; } void dfs(int g,int gc,int tc,int start) /*gc:最后一组的序号,gc:组内最后一元素序号; tc:当前组内数; start:搜到第几个数*/ { if(g>=ans) return ;/*当前组数已经小于已知的最小组数,直接舍弃当前子树*/ if(tc==n) { ans=g; return; }/*当前组已经搜完所有数,更新ans,并返回*/ bool flag=true; for(int i=start;i<n;i++){ if(!st[i]&&check(group[g],gc,i)) { group[g][gc] = a[i]; st[i]=true; dfs(g,gc+1,tc+1,i+1); st[i]=false;/*恢复现场,不选这个数而是将这个数加入到后边的组*/ flag=false;/**/ } } if(flag) dfs(g+1,0,tc,0); } /*深度优先遍历每次我们只要考虑最后一组:第一次将第一个放入第一组,深度优先遍历直到 遍历完所有的数,并将所有符合互质的加入第一组,递归下一层因为下一层因为下一层的start ==n且flag==true,直接开数组,重新递归未遍历的数,直到tc==n即所有数都在所有组中,更新 最大组数并返回到上一层的dfs处: 1.若上一层未开新数组:做出不选择第i个数的决定并且flag置为false,并判断i之后的数是否 有符合条件的,若有加入当前组,继续递归下一层,若没有符合互质的,则说明当前组除了第i 个数其余数不符合,直接返回上一层*/ int main() { cin>>n; ans=n; for(int i=0;i<n;i++) scanf("%d", a+ i); dfs(1,0,0,0); cout<<ans<<endl; return 0; }