深度优先遍历(DFS)例题

深度优先要比广度优先难一些。

一般来说,一个DFS由三段组成,依次是:结束搜索,状态更新,状态转换

举两个栗子

1. 神奇的口袋

有一个神奇的口袋,总的容积是40,用这个口袋可以变出一些物品,这些物品的总体积必须是40。John现在有n个想要得到的物品,每个物品的体积分别是a1,a2……an。John可以从这些物品中选择一些,如果选出的物体的总体积是40,那么利用这个神奇的口袋,John就可以得到这些物品。现在的问题是,John有多少种不同的选择物品的方式。

DFS解法:

  1. 结束搜索
    1.1 当剩余空间恰好为零时(成功)
    1.2 当剩余空间为负、或遍历完物品仍不满足时(失败)
  2. 状态更新
    对于每个物品,无非是放入口袋和不放入口袋两种情况,用dfs(id,rem)表示第id个物品放入后的剩余空间为rem,那么放入第id+1个物品的状态是dfs(id+1,rem-v[id]),不放入的状态便是dfs(id+1,rem)
  3. 状态转移
    递归调用两个状态即可。
#include<iostream>
using namespace std;
 
int cnt, n;
int v[20];
 
void dfs(int idx, int rem){  //idx是物品标号,rem是剩余空间 
    if(rem == 0){   
        cnt++;   //方式数加一 
        return;
    }
    if(rem < 0 || idx == n - 1)    //当空间为负、遍历到最后一个物品仍不符合背包条件时结束 
        return; 
    dfs(idx + 1, rem);  //两种状态转换 
    dfs(idx + 1, rem - v[idx + 1]);
}
 
int main(){
    while(cin >> n){
        cnt = 0;
        for(int i = 0; i < n; i++){
            cin >> v[i];
        }
        dfs(-1, 40);   //从-1开始是因为从第0个物品开始就要考虑要与不要两种情况,有点dp的感觉 
        cout << cnt << endl;
    }
    return 0;
}

但这样还是有一点动态规划的感觉,因为考虑了放与不放的两个状态转移。
还有一种更容易理解的dfs思想:对于每一次访问,都遍历所有还未被遍历的结点。若容量小于40,则继续遍历;等于40,则方式数加一;大于40,则回溯。

#include<iostream>
#include<string>
#include<sstream>
using namespace std;

int n;
int ways=0;
int volume[100]={0};
bool visit[100]={false};

void dfs(int index,int v)
{
//	cout << "index:" << index << " v:" << v << endl;
	if(v == 40){
		ways++;
	}		
	else if(v < 40){
		visit[index] = true;
		for(int i=index;i<=n;i++){ 
			if(visit[i])
				continue;
			dfs(i,v+volume[i]);//遍历所有还未被遍历的结点 
		}
		visit[index] = false;
	}
}

int main()
{
	cin >> n;
	for (int i = 1; i <= n; i++)  //volume[0]=0;从1开始存放每个物品
		cin >> volume[i];   
	dfs(0,0);   //要是从1开始就相当于使第一个物品必须放入背包 
	cout << ways << endl;
} 

2.A Knight’s Journey

Background

The knight is getting bored of seeing the same black and white squares again and again and has decided to make a journey around the world. Whenever a knight moves, it is two squares in one direction and one square perpendicular to this. The world of a knight is the chessboard he is living on. Our knight lives on a chessboard that has a smaller area than a regular 8 * 8 board, but it is still rectangular. Can you help this adventurous knight to make travel plans?

Problem

Find a path such that the knight visits every square once. The knight can start and end on any square of the board.
在这里插入图片描述

Input

The input begins with a positive integer n in the first line. The following lines contain n test cases. Each test case consists of a single line with two positive integers p and q, such that 1 <= p * q <= 26. This represents a p * q chessboard, where p describes how many different square numbers 1, . . . , p exist, q describes how many different square letters exist. These are the first q letters of the Latin alphabet: A, . . .

Sample Input
1
4 3

Sample Output
A1B3C1A2B4C2A3B1C3A4B2C4

此题并不难,不过有几点小启示:

  1. 可以使用string作为参数来记录递归的过程(路线),debug困难时也可以尝试用此方法来查找问题。(文后附用sstream类来进行字符串与数字互相转化的一种方法)
  2. 不可忘记取消visit标记。第30行的标记取消是因为此时需要回溯,就需要将该点置为未访问;而第19行的标记取消一直到求助了大佬后才想到。原因是我一直想不通为什么仅仅输出了一个解路线就会终止。(当然,该题目仅要求输出一条ascii码序最小的路线,不取消19行的标记才是正确的,因为这样可以抑制后面的输出。但要知道原因)
  3. 关于ascii码序如何处理。直接在遍历时考虑他们的相对位置。如图编号为1的点为B3,编号为2的点为B5,ascii中B3的优先级就高于B5,所以将B3排在前。这样最终得到的整个序列也必定是ascii序最小的序列。
    在这里插入图片描述
#include<iostream>
#include<string.h>
#include<string>
using namespace std;

int m, n;
bool visit[30][30];

void dfs(int row, int col, int steps, string s)
{
	if (visit[row][col] || row >= m || row < 0 || col >= n || col < 0)  //已访问或不合法
		return;
	visit[row][col] = 1;
	s += 'A' + col;
	s += '1' + row;
	steps++;
	if (steps == m * n) {
		cout << s << endl;
		visit[row][col] = 0;//要注意这一句
		return;
	}
	dfs(row - 1, col - 2, steps, s);
	dfs(row + 1, col - 2, steps, s);
	dfs(row - 2, col - 1, steps, s);
	dfs(row + 2, col - 1, steps, s);
	dfs(row - 2, col + 1, steps, s);
	dfs(row + 2, col + 1, steps, s);
	dfs(row - 1, col + 2, steps, s);
	dfs(row + 1, col + 2, steps, s);
	visit[row][col] = 0;
	return;
}

int main()
{
	cin >> m >> n;
	memset(visit, false, sizeof(visit));
	dfs(0, 0, 0, "");
	return 0;
}

附:

1. 字符串---->数字

以前都是用atof()、atoi()、atol() 实现这一系列转化,但总觉得不方便。索性学了sstream类,用C++中的流来实现这一目的。

#include<sstream>
#include<iostream>
#include<string>
using namespace std;

double toDoub(string s)
{
	istringstream i(s);//从s中创建输入流
	double d;
	i >> d;
	return d;
}

int main()
{
	int t;
	string s = "41 3 5 89 300";  //使用空格分隔(默认分隔符) 
	istringstream i(s);
	while(i >> t)
		cout << t << endl;
	
	cout << endl;
	string ss = "41.5,3.8,5.5,8.9,30.0"; //使用逗号',' 分隔 
	string tmp;
	istringstream ii(ss);
	while(getline(ii,tmp,',')){  //配合getline使用,先从整个字符串中提取出每一个小数,设置','为分隔符
		cout << toDoub(tmp) << endl; //再用一次输入流转化为double类型
	}
} 

2. 数字---->字符串

对于单个字符或许可以用char c = '0' + num;的形式来转化,但若是多位数字还这样做就太不方便了。当然也可以用C++11支持的to_string()方法。不过 ostringstream也可以解决这个问题。

#include<sstream>
#include<iostream>
using namespace std;

string toStr(int n)  //当然也可以是double
{
	ostringstream o;
	if(o << n)
		return o.str();
	return "";
}

int main()
{
	string st = "";
//	st += 1234;   //error
	st += toStr(1234); 
	cout << st << endl;
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值