乾坤未定,你我皆是黑马
例题1:在图上寻找路径
在图上如何寻找从1到8的路径
运气最好:1->2->4->8
运气稍差:1->2->4->5->6->8
运气坏:1-3->7->9=>7->A=>7=>3->5->6->8(双线箭头表示回退)
不连通的图,无法从节点1走到节点8
完整的尝试过程如下:
1->2->4->3->7=>3=>4=>2->9=>2=>1
结论:不存在从1到8的路径
得出这个结论之前,一定会把从1出发能走到的点全部都走过。
深度优先搜索(Depth-First-Search)DFS:
从起点出发,走过的点要做标记,发现有没走过的点,就随意挑一个往前走,走不了就回退。
C++:
int main(){
将所有的点都标记为新点;
起点=1;
终点=8;
cout<<Dfs(起点);
}
判断从V出发能否走到终点
bool Dfs(V){
if(V是终点)
return true;
else(v为旧点)//旧点:已经走过的点
return false;
将v标记为旧点;
对和v相邻的每个节点U{
if (Dfs(U)==true)
return true;
}
return false;
}
图的表示方法——邻接矩阵
用一个二维数组G存放图,G(i,j)表示节点i和节点j之间的边的情况(如有无边,边方向,权值大小等)。
图的表示方法——邻接表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5CsGl7Ul-1587027965341)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20200413133645849.png)]
每个节点V对应一个一维数组,里面存放从V连出去的边,边的嘻嘻包括另一个顶点,还可能包含边权值等信息。
1:2 3
2:1 4 9 3
3:1 4 7 2
4:2 3
5:6 8
6:5 8
7:3
8:5 6
9:2
遍历复杂度:O(n+3) n:节点数目,e:边数目
例题2:城堡问题(百炼2815)
右图是一个城堡的地形图,请你计算一共有多少房间,最大的房间有多大,城堡被分成m*n(m<=50,n<=50)个方块,每个方块可以有0-4面墙。
输入:
第一行是两个整数:分别是南北向、东西向的方块数。
在接下来的输入行里,每个方块用一个数字来表示,用一个数字来表示方块周围的墙,1:西墙;2:北墙;4:东墙;8:南墙。每个方块用代表其周围墙的数字之和表示。城堡的内墙被计算两次,方块(1,1)的南墙同时也是方块(2,1)的北墙。输入的数据保证城堡至少有两个房间。
输出:
城堡的房间数、城堡中最大房间所包含的方块数。
样例输入:
4 7
11 6 11 6 3 10 6
7 9 6 13 5 15 5
1 10 12 7 13 7 5
13 11 10 8 10 12 13
样例输出:
5 7
解题思路:
把方块看做节点,相邻两个方块之间如果没有墙,则在方块之间连一条边,这样城堡就能转换成一个图。
求房间个数,实际上就是求图中有多少个极大连通子图。
一个连通子图,往里头加任意一个图里的其他点,就会不连通,那个这个连通子图就是极大连通子图。(如:8,5,6)
#include <iostream>
#include <stack>
#include <cstring>
using namespace std;
int R,C;
int rooms[60][60];//存放方块
int color[60][60];//方块是否染色的标记
int maxRoomArea=0,roomNum=0;
int roomArea;//正在探索的那个房间的面积
void Dfs(i,k){
if (color[i][k])
return;
++roomArea;
color[i][k]=roomNum;
if (rooms[i][k] & 1 ==0) Dfs(i,k-1);
if (rooms[i][k] & 2 ==0) Dfs(i-1,k);
if (rooms[i][k] & 4 ==0) Dfs(i,k+1);
if (rooms[i][k] & 8 ==0) Dfs(i+1,k);
}
int main(){
cin>>R>>C;
for(int i=1;i<=R;i++)
for(int j=1;j<=C;j++)
cin>>rooms[i][j];
memset(color,0,sizeof(color));//所有的方块都没有走过
for (int i=1;i<=R;i++)
for(int j=1;j<=C;j++){
if (!color[i][j]){
++roomNum;roomArea=0;
Dfs(i,k);
maxRoomArea=max(roomArea,maxRoomArea);
}
}
cout<<roomNum<<maxRoomArea<<endl;
}
例题3:踩方格(百炼4982)
有个方格矩阵,矩阵边界在无穷远处。有如下假设:
- 只能移动一格
- 走过的格子立即塌陷无法再走第二次
- 只能向北、东、西三个方向走;
请问:如果允许在方格矩阵上走n步(n<=20),共有多少种不同的方案。
思路:
递归:从(i,j)出发,走n步的方案数,等于以下三项之和:
从(i+1,j)出发,走n-1步的方案数。前提:(i+1,j)还没走过
从(i,j+1)出发,走n-1步的方案数。前提:(i,j+1)还没走过
从(i,j-1)出发,走n-1步的方案数。前提:(i,j-1)还没走过
C++:
#include <iostream>
#include <cstring>
using namespace std;
int visited[30][50];
int ways(int i,int j,int n){
if(n==0)
return 1;//就是一步都不用走,就是不走,就是一种走法
visited[i][j]=1;
int num=0;
if (!visited[i][j-1])
num+=ways(i,j-1,n-1);
if (!visited[i][j+1])
num+=ways(i,j+1,n-1);
if (!visited[i+1][j])
num+=ways(i+1,j,n-1);
visited[i][j]=0; //关键
return num;
}
int main(){
int n;
cin>>n;
memset(visited,0,sizeof(visited));
cout<<ways(0,25,n)<<endl;//从(0,25)走n步的方案数
return 0;
}
例题:4:寻路问题
N个城市,标号1到N。城市之间有R条单向道路。
每条道路连接两个城市,有长度和过路费两个属性。
小安只有K块钱,想从城市1走到城市N。问最短需要走多长的路。如果到不了,输出-1
2<=N<=100
0<=K<=10000
0<=R<=10000
每条路的长度L,1<=L<=100
每条路的过路费T,0<=T<=100
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
int K,N,R;
struct Road{
int d,L,T; //终点,长度,费用
};
vector< vector< Road> > G(110);
int minLen;
int totalLen;
int totalCost;
int visited[110];
void dfs(int s){
if (s==N){
minLen=min(minLen,totalLen);
return ;
}
for(int i=0;i<G[s].size();i++){
Road r=G[s][i];//对起点为s的边进行遍历
if(totalCost+r.t>K)
continue;
if (!visited[r.d]){
totalLen+=r.L;
totalCost+=r.t;
visited[r.d]=1;
dfs(r.d);
visited[r.d]=0;
totalLen-=r.L;
totalCost-=r.t;
}
}
}
int main(){
cin>>K>>N>>R;
for (int i=0;i<R;i++){
int s;//起点
Road r;
cin>>s>>r.d>>r.L>>r.t;
if ( s!= r.d){
G[s].push_back(r);
}
}
memset(visited,0,sizeof(visited));
totalLen=0;
minLen=1<<30; //一个非常大的数
totalCost=0;
visited[1]=1; //走过第一个点了
dfs(1);
if (minLen<(1<<30)){
cout<<minLen<<endl;
}
else{
cout<<"-1"<<endl;
}
return 0;
}
上述运行:时间超时
最优剪枝:
- 如果当前已经找到的最优路径为L,那么在继续搜索的过程中,总长度大于L的走法,就可以直接放弃,不用走到底了。
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
int K,N,R;
struct Road{
int d,L,T; //终点,长度,费用
};
vector< vector< Road> > G(110);
int minLen;
int totalLen;
int totalCost;
int visited[110];
void dfs(int s){
if (s==N){
minLen=min(minLen,totalLen);
return ;
}
for(int i=0;i<G[s].size();i++){
Road r=G[s][i];//对起点为s的边进行遍历
if(totalCost+r.t>K)
continue;
if (!visited[r.d]){
//最优剪枝
if (totalLen+r.L>minLen)
continue;
totalLen+=r.L;
totalCost+=r.t;
visited[r.d]=1;
dfs(r.d);
visited[r.d]=0;
totalLen-=r.L;
totalCost-=r.t;
}
}
}
int main(){
cin>>K>>N>>R;
for (int i=0;i<R;i++){
int s;//起点
Road r;
cin>>s>>r.d>>r.L>>r.t;
if ( s!= r.d){
G[s].push_back(r);
}
}
memset(visited,0,sizeof(visited));
totalLen=0;
minLen=1<<30; //一个非常大的数
totalCost=0;
visited[1]=1; //走过第一个点了
dfs(1);
if (minLen<(1<<30)){
cout<<minLen<<endl;
}
else{
cout<<"-1"<<endl;
}
return 0;
}
上述程序,提交到OJ上还是超时:
修改思路:
用mid(k,m)表示:走到城市k时总过路费为m的条件下,最优路径的长度。若在后续的搜索时,再次走到k时,如果总路费恰好是m,且此时的路径长度已经超过mid(k,m)就不必再走下去了:
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
int K,N,R;
struct Road{
int d,L,T; //终点,长度,费用
};
vector< vector< Road> > G(110);
int minLen;
int totalLen;
int totalCost;
int visited[110];
int minL[110][10010];
void dfs(int s){
if (s==N){
minLen=min(minLen,totalLen);
return ;
}
for(int i=0;i<G[s].size();i++){
Road r=G[s][i];//对起点为s的边进行遍历
if(totalCost+r.t>K)
continue;
if (!visited[r.d]){
//最优剪枝
if (totalLen+r.L>minLen)
continue;
if (totalLen+r.L>=minL[r.d][totalCost+r.t])
continue;
minL[r.d][totalCost+r.t]=totalLen+r.L;
totalLen+=r.L;
totalCost+=r.t;
visited[r.d]=1;
dfs(r.d);
visited[r.d]=0;
totalLen-=r.L;
totalCost-=r.t;
}
}
}
int main(){
cin>>K>>N>>R;
for (int i=0;i<R;i++){
int s;//起点
Road r;
cin>>s>>r.d>>r.L>>r.t;
if ( s!= r.d){
G[s].push_back(r);
}
}
memset(visited,0,sizeof(visited));
totalLen=0;
minLen=1<<30; //一个非常大的数
totalCost=0;
visited[1]=1; //走过第一个点了
for (int i=0;i<110;i++)
for (int j=0;j<10010;j++)
minL[i][j]=1<<30;
dfs(1);
if (minLen<(1<<30)){
cout<<minLen<<endl;
}
else{
cout<<"-1"<<endl;
}
return 0;
}
例题5:切蛋糕
要制作一个体积是Nπ的M层生日蛋糕,每层都是一个圆柱体。
从下网上,高度和半径逐层递减。由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面的面积Q最小。
令Q=Sπ
编程时给出的N和M,找出蛋糕的制作方案,使得S最小
解题思路:
- 枚举每一层可能的高度和半径
- 如何确定搜索范围?底层蛋糕的最大可能半径和最大可能高度
- 搜索顺序,哪些地方体现搜索顺序?从底层向上
- 如何剪枝?
- 剪枝1:搭建过程中发现已经建好的面积已经超过目前求的的最优表面积,或者预见到完成后的面积一定会超过目前最优表面积,则停止搭建
- 搭建过程中预见到再往上搭,高度无法安排或者半径无法安排。
- 搭建过程中发现还没搭的那些层的体积一定会超过还缺的体积,则停止搭建。
- 搭建过程中发现还没搭的那些层的体积,最大也到不了还缺的体积,则停止搭建。
#include <iostream>
#include <vector>
#include <cstring>
#include <cmath>
using namespace std;
int N,M;
int minArea=1<<30;//最优表面积
int area=0;
int minv[30],mins[30];
void Dfs(int v,int n,int r,int h){
//n层蛋糕凑体积v,最底层半径不超过r,高度不超过h
if (n==0){ //如果层为0
if (v) //如果体积还剩,则不成功,返回
return ;
else{//n=0,v=0,成功
minArea=min(minArea,area);
return;
}
}
if (v<=0)
return;
//枚举第n层的高度和半径,第n层的半径和高度至少是n;
for( int rr=r;rr>=n;--rr){
if (n==M)//底面积
area=rr*rr;//第一次向area添加值
for (int hh=h;hh>=n;hh--){
area+=2*rr*hh; //每加一层就添加一个侧面积
//约束条件
if (area>minArea)
continue;
else
if (minArea-area<minv[n])
continue;
v_=v-rr*rr*hh;
if (v_<minv[n-1])
continue;
Dfs(v-rr*rr*hh,n-1,rr-1,hh-1);
area-=2*rr*hh;
}
}
}
int main(){
cin>>N>>M;
int maxR=(int)sqrt(N);
int maxH=M;
minv[0]=0;
mins[0]=0;
//设定每一层的最小面积和最小体积
for (int i=1;i<=M;i++){
minv[i]=min[i-1]+i*i*i;
mins[i]=min[i-1]+2*i*i;
}
Dfs(int N,M,maxR,maxH);
if (minArea==1<<30)
cout<<0<<endl;
else
cout<<minArea<<endl;
return 0;
}