一、图
1、有向图与无向图
有向图:相当于马路的单行线,只能单向通过
无向图:相当于一条普通的马路,既能来也能去,是双向均可实现的
2、权值与网
每条边可以标记数值,代表权值的意义,可以用来求最短路问题之类的
带权的图叫做网
3、连通图
两个顶点之间有路径则称二者是连通的,若图中任意两个顶点之间都连通,则称这张图为连通图
4、建边
对于一张图最重要的是建边,首先要把边建好才能知道当前节点能够到达哪些节点
做题时顶点数目是巨大的,如果用map矩阵存图
如下:
int mp[n][n];
eg.
x->y 表示x到y有一条边
//无向边:x到y有一条边,y到x也有一条边,是要建两条边的
mp[x][y]=1; //代表x到y有一条边
/*
但是如果n是1e5这样的级别的话
这样的二维矩阵是开不下的
而且一一遍历时的时间复杂度过高
*/
因此利用map建边不可取!!!
常见的建边方式有两种:
①利用vector容器
vector<int>G[n]; //如果边有权值 可以把int改为一个结构体 直接把整个结构体推进去即可
//结构体中存入①该点连出去的点 ②边的权值
void way1() //类似于数组的形式 直接一个个推进去就好了
{
G[x].push_back(y); //加边方式
//x对应遍历一遍 即从x连出去的所有点
int sz=G[x].size(); //获得x连出去的所有点
//x连出去多少点 size就有多大 遍历sz时的时间复杂度就会降低
for(int i=0;i<sz;i++)
int to=G[x][i]; //to即为x连出去的所有点
}
②链式前向星
int tot,ver[n*2],next[n*2],head[n];
//tot:给每个边编号
//ver数组:存储每个编号的边连出去的点
//next数组:该往前连接的边的编号(若没有则为0)
//head数组:该最后一个编号的边
void add(int u,int v) //添加一条从u->v的边
{
++tot;
ver[tot]=v;
next[tot]=head[u];
head[u]=tot;
}
void way2() //链式前向星 有比较多数组
{
add(x,y); //加边方式
for(int i=head[x];i;i=next[i]) //通过遍历获取跟u相连的所有点的编号
{ //终止条件是i!=0 (i=0就是没有边了)
int to=ver[i]; //to即为x这条边所连出去的所有的点
}
}
int main()
{
memset(head,0,sizeof head); //全部置0 相当于初始化为每条边没有指向
/* 主函数 */
}
二、BFS
1、理解
遍历时可以类比于波一样的扩散方式,遍历距离一条边的,两条边的,三条边的…直到全部遍历完
(更多详情看优质blog传送门:BFS广度优先搜索)
2、模板示例
- 以HDU 2612为例:
Find a way
- 题意: 给定n,m两个整数,代表这是一个n*m大小的字符矩阵,‘Y’,‘M’代表两个人,‘#’代表此路不通,不能走这个点,‘.’代表道路,可以走这个点,‘@’代表kfc,两人要到kfc汇合,上下左右都可以走,每走一步花费11min,求最短的时间总和
- 输入: 输入包含多个测试用例。每个测试用例包括前两个整数n,m.(2<=n,m<=200)。接下来的n行,每行包含m个字符。“Y”和“M”表示两人初始位置。“#”禁止上路;’.'路。“@”KCF
- 输出: 对于每个测试用例,输出两人到达其中一个肯德基的最短总时间。
Sample Input
4 4
Y.#@
…
.#…
@…M
4 4
Y.#@
…
.#…
@#.M
5 5
Y…@.
.#…
.#…
@…M.
#…#
Sample Output
66
88
66
方法①
#include<bits/stdc++.h> //bfs
using namespace std;
#define pii pair<int,int> //pair相当于存储一对东西
//pii Point=make_pair(x,y); //例如要将x和y进行绑定
//x= Point.first; //获取x y= Point.second; //获取y //这样就能把这一对获取完
const int N=200+10;
const int M=10;
const int inf=0x3f3f3f3f; //inf趋近于无穷大
char s[N][N]; //代表每个点
int sx[M],sy[M]; //代表起点信息 //M=0代表‘Y’这个人,M=1代表‘M’这个人 (以下数组同理)
int n,m,dis[M][N][N]; //因为有两个人,每个人对应的dis数组不一样,所以开了三维数组
int x,y,dx[M]={0,0,1,-1},dy[M]={1,-1,0,0}; //dx和dy代表上下左右的坐标变化(***)
void bfs(int id){ //需要队列来实现 //id代表当前是哪一个人
queue<pii>q; //先进先出
q.push(make_pair(sx[id],sy[id]));
dis[id][sx[id]][sy[id]]=0; //把这个人所在的起点置零
while(!q.empty())
{
int x=q.front().first,y=q.front().second; //获取当前队首的点
q.pop(); //不能忘记!!! 否则会卡死在这里
for(int i=0;i<4;i++)
{
int xx=x+dx[i],yy=y+dy[i]; //代表下一步要走的点 //把上下左右依次都走一遍试试
if(1<=x&&xx<=n&&1<=yy&&yy<=m&&dis[id][xx][yy]==inf&&s[xx][yy]!='#') //if内判断能不能走
{ //xx和yy要在矩阵范围内 //若dis为inf说明还没走过这个点 那就可以走 //并且下一步不能是死胡同
dis[id][xx][yy]=dis[id][x][y]+1; //则将下一步置为出发点+1的距离
q.push(make_pair(xx,yy)); //再将下一步的这个点推进去
} //以此类推,直到将整张图的点遍历完
} //那么这个人从起点到达任意一个点的最短距离都可以求出来了
}
}
int main()
{
while(scanf("%d %d",&n,&m)!=EOF)
{
memset(dis,inf,sizeof dis);
for(int i=1;i<=n;i++) //读入矩阵
scanf("%s",s[i]+1);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(s[i][j]=='Y') //找坐标起点 //横坐标是i 纵坐标是j
{
sx[0]=i;
sy[0]=j;
}
if(s[i][j]=='M')
{
sx[1]=i;
sy[1]=j;
}
}
}
bfs(0),bfs(1); //因为有两个人所以要bfs两次
int res=inf; //result 代表总步数
for(int i=1;i<=n;i++) //开始遍历
{
for(int j=1;j<=m;j++)
{
if(s[i][j]=='@')
res=min(res,dis[0][i][j]+dis[1][i][j]); //求出这两个人到达kfc的最短距离
}
}
printf("%d\n",res*11); //走一步需要11min 所以最后总步数*11=总时间
}
}
其实一开始别的都能马上理解,唯独(***)的部分突然就看不懂了,就是理解不了用dx、dy数组去标记方向变化的原理,wsr给我讲了好多还是迷迷糊糊的(呜呜呜呜我有点子笨笨),然后他把他的码发我了,结果他的码我除了表示方向变化的moven数组别的也都懂,那么问题还是同一个。经过艰难的抽象交流后,我终于有点子明白了。wsr是用二维数组moven直接记录横纵坐标,上面那个码是dx和dy分开记录了横纵坐标,但其实原理是一样的。
通过数组表示的x、y的1或-1的值来表示上下左右的移动方向
这里再贴一个wsr的码,他是用了结构体去推进队列:
方法②
#include<bits/stdc++.h> //wsr的pro a 码
using namespace std;
#define inf 0x3f3f3f3f
const int maxn = 2e2 + 10;
int n, m, d1[maxn][maxn], vis[maxn][maxn], d2[maxn][maxn], ans;
char mp[maxn][maxn];
int moven[5][5] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
struct Node{
int x, y, d;
};
bool check(int x, int y) {
if(x < 1 || x > n || y < 1 || y > m || vis[x][y] || mp[x][y] == '#') return false;
return true;
}
void BFS(int x, int y) {
ans++;
memset(vis, 0, sizeof(vis));
Node tmp;
tmp.x = x, tmp.y = y, tmp.d = 0;
queue<Node>q;
q.push(tmp);
vis[x][y] = 1;
while(!q.empty()) {
Node now = q.front();
q.pop();
for(int i = 0; i < 4; i++) {
int mx = now.x + moven[i][0];
int my = now.y + moven[i][1];
if(check(mx, my)) {
vis[mx][my] = 1;
tmp.x = mx, tmp.y = my, tmp.d = now.d + 1;
q.push(tmp);
if(mp[mx][my] == '@') {
if(ans == 1)
d1[mx][my] += tmp.d;
else
d2[mx][my] += tmp.d;
}
}
}
}
}
int main() {
while(~scanf("%d %d", &n, &m)) {
ans = 0;
for(int i = 1; i <= n; i++)
scanf("%s", mp[i] + 1);
memset(d1, 0, sizeof(d1));
memset(d2, 0, sizeof(d2));
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
if(mp[i][j] == 'Y' || mp[i][j] == 'M')
BFS(i, j);
}
}
int minn = inf;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
if(d1[i][j] == 0 || d2[i][j] == 0) continue;
else{
minn = min(d1[i][j] + d2[i][j], minn);
}
}
}
printf("%d\n", minn * 11);
}
system("pause");
return 0;
}
三、DFS
1、介绍
优质blog推荐:广搜和深搜
这篇里面关于dfs的图形讲解很好理解
dfs可以用来暴搜,如果要在暴搜的基础上对其进行优化,那么就要用到剪枝,可以剪掉一些不必要的搜索,从而大大降低时间复杂度
剪枝相关blog传送门:剪枝
2、模板示例
- 以HDU 2553 为例
N皇后(经典)
- 题意: 在N*N的方格棋盘放置了N个皇后,使得它们不相互攻击(即任意2个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成45角的斜线上。对于给定的N,求出有多少种合法的放置方法。
- 输入: 共有若干行,每行一个正整数N≤10,表示棋盘和皇后的数量;如果N=0,表示结束。
- 输出: 共有若干行,每行一个正整数,表示对应输入行的皇后的不同放置数量。
(注: 查询若干行,可以预先处理出1-10的所有答案,再询问直接出答案)
Sample Input
1
8
5
0
Sample Output
1
92
10
#include<bits/stdc++.h>
using namespace std;
const int N =10+10;
int f[N],vis[N],n,cnt;
bool check(int row,int col)
{ //check函数判断第row行第col列能不能放置棋子
if(vis[col])return false; //vis[col]表示的是第col列放置了第几行 //eg. vis[2]=3表示第2列放置了第3行
for(int i=1;i<=n;i++) //按列依次判断45°的情况
if(vis[i] && abs(i-col)==abs(vis[i]-row)) return false; //if判断条件后者:abs(列-列)=abs(行-行) 即为45°线上
return true;
}
void dfs(int now)
{ //now表示当前准备放置第now行
if(now==n+1) //能够放置到第n+1行说明前面的n行都成功放置了,那么说明这种方法也可以
{
cnt++;
return;
}
for(int i=1;i<=n;i++)
{
if(check(now,i)) //判断第now行第i列能不能放(算是剪枝操作)
{ //如果可以放
vis[i]=now; //标记第i列放置了第now行
dfs(now+1); //放置下一行
vis[i]=0; //还原 当作没放过 才不会影响对下一行的重新判断
}
}
}
int main(){
for(n=1;n<=10;n++)
{
memset(vis,0,sizeof vis);
cnt=0; //初始化
dfs(1); //每次从第一行开始放起
f[n]=cnt; //记录每个N值对应的种数
}
int x;
while(scanf("%d",&x)!=EOF)
{
if(x==0) break;
printf("%d\n",f[x]);
}
}
注: 1.题中45°并非只是主对角线和副对角线,而是只要与棋盘成45°的都不能放两个棋子
2.对vis数组的含义一定要理清楚,它是起到标记作用的
3.在依次判断的过程中,一定要牢记初始化,不管是种数cnt也好还是vis数组也好,都要记得初始化
4.为了降低时间复杂度,我们可以具体问题具体分析,用if判断条件来实现剪枝的操作,过滤掉不必要的搜索
5.对于n上限小的题目,可以预处理所有n取值的情况,最后直接访问数组输出对应种数,避免tle
(一些例题下次再发 先放过自己)