深度优先搜索 (DFS)

深度优先搜索 (DFS)

先根遍历:根、左子树、右子树 中根遍历:左子树、根、右子树 后根遍历:左子树、右子树。根

引例

题目描述

输入一个正整数 n,按照字典序输出 1n 的全排列,每个排列占一行,数字间用空格分隔。

示例输入 示例输出
3 1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

字典序:dfs使用VIS标记和 vis[i]=0 回溯

基本模型

深度优先搜索的基本模型:

void dfs(int step)
{
特殊情况处理 (一般是结束递归的情况)
枚举当前每一种可能 for(i=1; i<=n; ++i)
在枚举的每一种可能中, 递归dfs(step+1);
}

C++ 实现示例

#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;

bool vis[12];
int num[12];
int n;

void dfs(int steps){
if(steps==n+1){
for(int i=1;i<=n;i++)
printf("%d",num[i]);
printf("\n");
return ;
}
for(int i=1;i<=n;i++){
if(!vis[i]){
vis[i]=1;
num[steps]=i;
dfs(steps+1);
vis[i]=0;
}
}
return ;
}

int main(){
while(scanf("%d",&n)!=EOF){
memset(vis,0,sizeof(vis));
dfs(1);
}
return 0;
}

例题二:迷宫搜索

题目名: B. 手机的诱惑 运行时间限制: 1000ms 运行内存限制: 32768kb ### 题目描述 张晨乐在一个古老的迷宫中发现了一个手机,这个手机深深地吸引了他。然而,当他拿起手机,迷宫开始摇晃,张晨乐能感觉到地面下沉。他意识到:这个手机只是一个诱饵!于是,他不顾一切地试图冲出这个迷宫。

迷宫是一个大小为N*M的矩形,有一扇门,一开始,门是关闭的,并在第T秒打开一瞬间(小于1秒的时间)。因此,张晨乐必须刚好在第T秒钟到达门口。

每一秒,他都可以向上,下,左,右四个相邻的位置中的任意一个移动。一旦他进入一个新的地方,这个地方的地面就会开始下沉,并在下一秒消失。因此,他不能在一个地方停留超过一秒钟,也不能再进入曾经走过的地方。

请问,可怜的张晨乐能够逃出迷宫吗?

输入

输入由多个测试用例组成。

每个测试用例的第一行包含三个整数N, M和T (1 < N, M < 7; 0 < T < 50),分别表示迷宫的大小和门打开的时间。

接下来的N行给出迷宫布局,每行包含M个字符。

每个字符含义如下: - ‘X’ : 不能进入的墙 - ‘S’ : 起点 - ‘D’ : 门 - ‘.’ : 可以行走的地方

输入以三个0结束,这个测试用例不被处理。

输出

对于每组测试数据,如果张晨乐能够逃出迷宫,则请输出”YES”,否则,请输出”NO”。每组数据输出占一行。

输入样例

4 4 5
S.X.
..X.
..XD
····
3 4 5
S.X.
..X.
...D
0 0 0

输出样例

NO
YES

奇偶性剪枝:

0->1 一定是奇数次

算法细节:走过的标记为wall ’X’,回溯时重置为road ‘.’

#include <cstdio>
#include <iostream>
#include <cstring>
#include <cmath>
using namespace std;

int n,m,t;
int wall;
int x,y,xx,yy;
int cx[]={1,0,-1,0};
int cy[]={0,1,0,-1};
char map[10][10];
bool escape;

void dfs(int sx,int sy,int cnt){
if(escape){
return ;
}
if(sx<=0 sx>n sy<=0 sy>m){
return ;
}
if((t-cnt-abs(sx-xx)-abs(sy-yy))<0){
return ;
}
if(cnt==t && sx==xx && sy==yy){
escape=1;
return ;
}
for(int i=0;i<4;i++){
if(map[sx+cx[i]][sy+cy[i]]!='X'){
map[sx+cx[i]][sy+cy[i]]='X';
dfs(sx+cx[i],sy+cy[i],cnt+1);
map[sx+cx[i]][sy+cy[i]]='.';
}
}
}

int main(){
while(scanf("%d%d%d",&n,&m,&t),nmt){
wall=0;
escape=0;
memset(map,0,sizeof(map));
getchar();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%c",&map[i][j]);
if(map[i][j]=='S'){
x=i;
y=j;
map[i][j]='X';
}
else if(map[i][j]=='X'){
wall++;
}
else if(map[i][j]=='D'){
xx=i;
yy=j;
}
}
getchar();
}
//TEST OUT
// for(int i=1;i<=n;i++){
// for(int j=1;j<=m;j++){
// cout<<map[i][j];
// }
// printf("\n");
// }
if(m*n-wall<=t){
cout<<"NO"<<endl;
continue;
}
int temp=t-abs(x-xx)-abs(y-yy);
if(temp%2==1){
cout<<"NO"<<endl;
continue;
}
dfs(x,y,0);
if(escape){
cout<<"YES"<<endl;
}
else{
cout<<"NO"<<endl;
}
}
return 0;
}

DFS二分图匹配:

匈牙利算法: N 次 DFS 子问题分解

### 例题:Problem Description RPG girls

今天和大家一起去游乐场玩,终于可以坐上梦寐以求的过山车了。可是,过山车的每一排只有两个座位,而且还有条不成文的规矩,就是每个女生必须找个个男生做partner和她同坐。但是,每个女孩都有各自的想法,举个例子把,Rabbit只愿意和XHD或PQK做partner,Grass只愿意和linle或LL做partner,PrincessSnow愿意和水域浪子或伪酷儿做partner。考虑到经费问题,boss刘决定只让找到partner的人去坐过山车,其他的人,嘿嘿,就站在下面看着吧。聪明的Acmer,你可以帮忙算算最多有多少对组合可以坐上过山车吗?

Input 输入数据的第一行是三个整数K , M , N,分别表示可能的组合数目,女生的人数,男生的人数。0<K<=1000 1<=N 和M<=500.接下来的K行,每行有两个数,分别表示女生Ai愿意和男生Bj做partner。最后一个0结束输入。

Output 对于每组数据,输出一个整数,表示可以坐上过山车的最多组合数。

#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;

bool map[2020][2020];
int links[2020];
bool used[2020];
int k,n,m;

bool dfs(int s){
for(int j=1;j<=m;j++){
if(!used[j] && map[s][j]){
used[j]=1;
if(links[j]==0 dfs(links[j])){
links[j]=s;
used[j]=0;
return true;
}
used[j]=0;
}
}
return false;
}

int hungary(){
int ans=0;
memset(links,0,sizeof(links));
for(int i=1;i<=n;i++){
memset(used,0,sizeof(used));
if(dfs(i)){
ans++;
}
}
return ans;
}

int main(){
while(scanf("%d",&k),k){
cin>>n>>m;
memset(map,0,sizeof(map));
//重置操作
for(int i=0;i<k;i++){
int x,y;
scanf("%d%d",&x,&y);
map[x][y]=1;
}
cout<<hungary()<<endl;
}

return 0;
}

变式题目:

  1. 最小顶点覆盖:最少量的顶点链接所有边 数量等同于二分匹配

有两台机器A和B以及N个需要运行的任务。

每台机器有多种不同的模式,而每个任务都恰好在一台机器上运行。如果它在机器A上运行,则机器A需要设置为模式 a[i],如果它在机器B上运行,则机器B需要设置为模式b[j]。每台机器上的任务可以按照任意顺序执行,但是每台机器每转换一次模式需要重启一次。

请合理为每个任务安排一台机器并合理安排顺序,使得机器重启次数尽量少。

#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;

bool map[110][110];
int links[110];
bool used[110];
int n,m,k;

bool dfs(int s){
for(int j=1;j<=m;j++){
if(!used[j] && map[s][j]){
used[j]=1;
if(links[j]==0 dfs(links[j])){
links[j]=s;
return true;
}
used[j]=0;
}
}
return false;
}

int hungary(){
memset(links,0,sizeof(links));
int ans=0;
for(int i=1;i<=n;i++){
memset(used,0,sizeof(used));
if(dfs(i)){
ans++;
}
}
return ans;
}

int main(){
while(scanf("%d",&n),n){
cin>>m>>k;
memset(map,0,sizeof(map));
for(int i=0;i<k;i++){
int id,x,y;
scanf("%d%d%d",&id,&x,&y);
if(x==0 y==0){
continue;
}
map[x][y]=1;
}
cout<<hungary()<<endl;
}
return 0;
}

  1. DAG图最小路径覆盖:(DAG:有向无环图) 最少的路覆盖所有的点 例题:

有一个城镇,它的所有街道都是单行的,并且每条街道都是和两个路口相连。同时已知街道不会形成回路。

你的任务是编写程序求最小数量的伞兵,这些伞兵可以访问(visit)所有的路口。对于伞兵的起始降落点不做限制。

解题方法:拆点形成二分图:

DAG最小路径覆盖数=节点数-二分图最大匹配数 核心

最大独立集:

独立节点数=节点数-最大匹配数

组合游戏:博弈论

定义:有规则,有限,无法继续进行为败者 必败点(P点):前一个选手取胜 必胜点(N点):下一个选手取胜

  1. 所有的终结点都是必败点
  2. 从必胜点一定有方法到必败点
  3. 从必败点只能走到必胜点

算法eg1

取子游戏: S = {1, 3, 4}

第一步:标记所有终结点为必败点P 第二步:标记所有一步到P的点为必胜点 第三步:标记所有操作都到达N的点为必胜点 第四步:若没有新的P点,跳出 ———-有新的P点,回到二

疑问:如果三个人呢 ???????

eg2:玩游戏的小男孩

------------
------------
------------
------1----
-----S-----
------------
------------
------------
------2----
-----3------
------------
------------
------------
------------
------------
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;

int main(){
int t;
cin>>t;
while(t--){
int n,m;
cin>>n>>m;
if(n%(m+1)==0){
cout<<"Rabbit"<<endl;
}
else{
cout<<"Grass"<<endl;
}
}

return 0;
}

可以想为两堆牌(1,0)(0,1)(1,1)

eg3:nim游戏

从三堆(5,7,9)中选一堆任取扑克牌: nim-Sum两数按位异或得出结果

对于位置各堆nim和为0:则为必败点

NIM图游戏状态转移 sg函数:定义:不等于后继节点sg值的最小的非负整数 需要记忆化DFS

性质定理1:sg值>0 必胜点 ————sg值=0 必败点 定理2:大sg值为小sg值nim和

例子:

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

int f[1010];
int k,m;
int s[1010];
int h[1010];

int sg(int ss){
bool g[1010]={0};
for(int i=0;i<k;i++){
int t=ss-s[i];
if(t<0){
break;
}
if(f[t]==-1){
f[t]=sg(t);
}
g[f[t]]=1;
}
for(int i=0;i<1010;i++){
if(!g[i]){
return i;
}
}
}

int main(){
while(scanf("%d",&k),k){
memset(f,-1,sizeof(f));
for(int i=0;i<k;i++){
scanf("%d",&s[i]);
}
sort(s,s+k);
cin>>m;
for(int i=0;i<m;i++){
int cc;
cin>>cc;
int ans=0;
for(int j=0;j<cc;j++){
int temp;
cin>>temp;
ans^=sg(temp);
}
if(ans){
cout<<"W";
}
else{
cout<<"L";
}
}
cout<<endl;
}

return 0;
}

记忆化递归 在递归前判断是否已经算过 eg例题:

题目描述:

老鼠在某个城市储存了一些奶酪,城市可以看成一个n*n的网格,在每个网格的洞里,老鼠存储了0~100块的奶酪。现在,它准备享用这些美味。开始,它在(0,0)的位置,每吃光一个地方的奶酪,就转移到边上水平或者垂直的位置。麻烦的是,有只猫在洞的附近,所以它每次在被猫抓住之前最多只能移动k个位置,更糟糕的是,每吃掉一个地方的奶酪,就会变胖。所以,为了下一次逃跑有足够的能量,每次它必须移动到一个比这次有更多奶酪的网格。

给定n和k以及每个网格的奶酪数量;

请计算:老鼠最多能吃到多少块奶酪(直到不能继续活动)?

  • 可以dp 线性化:将所有点按奶酪数量排序,即可dp