信息学奥赛一本通之基础算法——第五章 搜索与回溯算法

信息学奥赛一本通之基础算法——第五章 搜索与回溯算法

链接索引🔗:

第五章 搜索与回溯算法

特别注意这一章的题目相对于其他算法题目来说比较难,除动态规划外,算是最难的题目。不过,细心 + 基础扎实 + 聪明 = 成功!

废话不多说,开始!

1317:【例5.2】组合的输出

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

// dfs函数用于深度优先搜索
void dfs(int start, int n, int r, vector<int>& ***bo) {
    // 如果组合的长度等于r,打印当前组合
    if (***bo.size() == r) {
        for (int i = 0; i < r; i++) {
            cout << "  " << ***bo[i]; // 每个元素占三个字符的位置
        }
        cout << endl;
        return;
    }

    // 从start到n遍历每个可能的元素
    for (int i = start; i <= n; i++) {
        ***bo.push_back(i); // 将当前元素添加到组合中
        dfs(i + 1, n, r, ***bo); // 递归调用,注意i+1作为新的起点
        ***bo.pop_back(); // 回溯,移除最后一个元素,尝试下一个可能的元素
    }
}

int main() {
    int n, r;
    cin >> n >> r; // 从用户输入读取n和r的值

    vector<int> ***bo; // 用于存储当前的组合
    dfs(1, n, r, ***bo); // 从1开始搜索

    return 0;
}
#include <bits/stdc++.h>
using namespace std;
int n, r;
vector<int> ans;
vector<bool> vis;

void dfs(int dep);

int main(){
	cin >> n >> r; 
	ans.resize(n + 10, 0);
	vis.resize(n + 10, false);
	dfs(1);
	return 0;
}

void dfs(int dep){
	if (dep == r + 1){
		for (int i = 1; i < dep; i++){
			cout << setw(3) << ans[i];
		}
		cout << endl;
	}
	for (int i = ans[dep - 1] + 1; i <= n; i++){
		if (!vis[i]){
			vis[i] = 1;
			ans[dep] = i;
			dfs(dep + 1);
			vis[i] = 0;
		}
	}	
}

两种方法自己选,我做的是第二种

1318:【例5.3】自然数的拆分

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

void dfs(int n, vector<int>& path, int start, int originalN) {
    if (n == 0 && path.size() > 1) { // 确保路径包含至少两个数字
        cout << originalN << "=";
        for (size_t i = 0; i < path.size(); ++i) {
            cout << path[i];
            if (i < path.size() - 1) cout << "+";
        }
        cout << endl;
        return;
    }
    
    for (int i = start; i <= n; ++i) {
        path.push_back(i);
        dfs(n - i, path, i, originalN);
        path.pop_back();
    }
}

int main() {
    int n;
    cin >> n;

    vector<int> path;
    dfs(n, path, 1, n);
    return 0;
}

1212:LETTERS

· 使用一个整数(假设是32位)的每一位来代表一个字母是否被访问过。由于大写字母总共有26个,因此我们只需要26位就足够了。例如,如果’A’被访问过,那么我们就将第0位设为1。
· 每次访问一个新的字母时,我们检查相应的位是否为0(表示该字母还未被访问过)。如果是,则继续DFS;否则,回溯。
· 通过这种方式,我们可以快速检查任何字母的访问状态,并且在回溯时轻松地撤销状态更改。
#include <iostream>
#include <vector>
using namespace std;

int r, s, maxl;
vector<string> a;

void dfs(int x, int y, int len, int vis){
	static const int fx[4] = {0, 0, 1, -1};
	static const int fy[4] = {1, -1, 0, 0};
	maxl = max(maxl, len);
	for (int i = 0; i < 4; i++){
		int tx = x + fx[i];
		int ty = y + fy[i];
		if (tx >= 0 && tx < r && ty >= 0 && ty < s){
			int b = 1 << (a[tx][ty] - 'A');	//位运算 
			if (!(vis & b)){
				dfs(tx, ty, len + 1, vis | b);
			}		
		}		
	}
}

int main(){
	cin >> r >> s;
	a.resize(r);
	for (int i = 0; i < r; i++){
		cin >> a[i];
	}
	int f = 1 << (a[0][0] - 'A');
	dfs(0, 0, 1, f);
	cout << maxl << endl;
	return 0;
}

这段代码通过位运算和DFS相结合的方式来避免重复访问字母,并且可以有效地解决超时问题。通过这种方式,我们不仅能够保证不会重复访问相同的字母,还能以更高效的方式追踪每条路径上的字母状态,从而优化整个搜索过程。
当使用位运算来跟踪已访问过的字母时,我们实际上是将一个整数的每一位用作一个标志位,来表示对应的字母是否已经被访问过。这种方法非常高效,因为它允许我们在常数时间内检查和更新访问状态。

让我们以一个简化的例子来演示这个过程。

示例

假设字母矩阵如下:

AB
CD
1
2
并且我们从左上角的A开始移动。

初始状态:

访问A,我们设置一个整数visited来跟踪访问状态。由于A是第一个字母,我们将visited的第0位设为1。如果用二进制表示,visited = 0001(这里为了简化只展示了4位,实际上需要26位来表示所有大写字母)。
移动到B:

接下来,我们尝试移动到B。B对应于visited中的第1位。我们检查这一位是否为0,发现是的(因为visited = 0001),这意味着B尚未被访问。
我们将B标记为已访问,通过将visited与0010进行OR操作(|),结果是visited = 0011。
尝试移动到C:

从B我们尝试向下移动到C。同样,C对应于visited中的第2位。检查这一位,发现是0(visited = 0011)。
标记C为已访问,通过将visited与0100进行OR操作,结果是visited = 0111。
回溯到B并尝试其他方向:

假设我们回溯到B尝试其他方向,此时visited不变,仍然为0111。
但由于A和C已经在visited中标记为已访问,我们不能再访问它们。

1213:八皇后问题

如果不知道国际象棋中的皇后怎么走的话,可以先看一下 1214:八皇后 中的介绍

这道题用回溯来做,大家可以先模拟四皇后的问题,进而推导出八皇后!

回溯:如果位置被一个皇后堵死了,那么就退回去(回溯),重新找!
#include <iostream>
#include <vector>
using namespace std;
int num = 1;
vector<vector<int> > p;    

bool check(int x, int y);
void dfs(int step);

int main(){
	p.resize(10, vector<int>(10));
	dfs(1);
	return 0;
}  

bool check(int x, int y){
	for (int i = 1; i <= 8; i++){
		if (p[i][y]) return false;  
	}
	for (int j = 1; j <= 8; j++){
		if (p[x][j]) return false;  
	}
	for (int i = 1; i <= 8; i++){
		for (int j = 1; j <= 8; j++){
			if (i - j == x - y && p[i][j]) return false;
		}
	}
	for (int i = 1; i <= 8; i++){
		for (int j = 1; j <= 8; j++){
			if (i + j == x + y && p[i][j]) return false;
		}
	}
	return true;
}

void dfs(int step){
	if (step == 9){
		cout << "No. " << num++ << endl;
		for (int i = 1; i <= 8; i++){
			for (int j = 1; j <= 8; j++){
				cout << p[j][i] << " ";
			}
			cout << endl;
		}
		return;
	}
	for (int j = 1; j <= 8; j++){
		  if (check(step, j)){
		  	p[step][j] = 1;   
		  	dfs(step + 1);
		  	p[step][j] = 0;
		  }
	}
}

1214:八皇后

#include <bits/stdc++.h>
using namespace std;

int dfs(int), print();  

int a[9];  //a数组用来输出(记录)皇后串
bool b[9], c[17], d[17];    	//标记数组
int ans, n;

int main()
{
	//全部默认标记没被占领
	memset(b, false, sizeof(b));
	memset(c, false, sizeof(c));
	memset(d, false, sizeof(d));
	int k;
	cin >> k;
	for (int i = 1; i <= k; i++)
	{
		ans = 0;
		cin >> n;
		dfs(1);   //从1开始递归
	}
	return 0;
}

//回溯求解函数
int dfs(int i)
{
	for (int j = 1; j <= 8; j++)	//搜索可以放的8个位置
	{
		if (b[j] == false && c[i + j] == false && d[i - j + 7] == false)
		{
			a[i] = j;		//记录列
			b[j] = true;	//占领过的列记录为真
			c[i + j] = true;
			d[i - j + 7] = true;	//占领两个对角线(d数组)
			if (i == 8) 
			{
				print();	//如果满足条件,直接输出
			}
			else
			{
				dfs(i + 1);		//否则递归
			}
			b[j] = false;	//回溯法,收回上一个放出过的皇后
			c[i + j] = false;
			d[i - j + 7] = false;
		}
	}
	return 0;
}

//打印函数
int print()
{
	ans++;
	if (ans == n)
	{
		for (int i = 1; i <= 8; i++)
		{
			cout << a[i];		//输出皇后串
		}
		cout << endl;
	}
	 return 0;
}

1215:迷宫

注意:auto之类的要求编译器是C++11哦!

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

bool dfs(vector<string>& maze, vector<vector<bool>>& visited, int ha, int la, int hb, int lb) {
    int n = maze.size();
    if (ha < 0 || ha >= n || la < 0 || la >= n || maze[ha][la] == '#' || visited[ha][la]) {
        return false;
    }
    if (ha == hb && la == lb) {
        return true;
    }
    
    visited[ha][la] = true;
    vector<int> fx = {0, 0, 1, -1};
    vector<int> fy = {1, -1, 0, 0};
    for (int i = 0; i < 4; ++i) {
        int new_ha = ha + fx[i];
        int new_la = la + fy[i];
        if (dfs(maze, visited, new_ha, new_la, hb, lb)) {
            return true;
        }
    }
    
    return false;
}

bool can_escape(vector<string>& maze, int ha, int la, int hb, int lb) {
    int n = maze.size();
    vector<vector<bool>> visited(n, vector<bool>(n, false));
    return dfs(maze, visited, ha, la, hb, lb);
}

int main() {
    int k;
    cin >> k;
    for (int t = 0; t < k; ++t) {
        int n;
        cin >> n;
        vector<string> maze(n);
        for (int i = 0; i < n; ++i) {
            cin >> maze[i];
        }
        int ha, la, hb, lb;
        cin >> ha >> la >> hb >> lb;
        if (can_escape(maze, ha, la, hb, lb)) {
            cout << "YES" << endl;
        } else {
            cout << "NO" << endl;
        }
    }
    return 0;
}

1216:红与黑

注意:这里很多变量都是反的!我连续做了四五次才 AC!

思路:

初始化: 创建一个二维数组作为地图,用来存储每个瓷砖的颜色,同时记录地图的宽度W和高度H。

找到起始点: 遍历地图,找到标记为’@'的瓷砖,这是起始点。记录起始点的位置。

深度优先搜索:

从起始点开始,探索所有可以到达的黑色瓷砖(‘.’)。
对于当前的瓷砖,尝试向四个方向(上、下、左、右)移动。
如果下一个瓷砖是黑色的,并且之前没有访问过,那么递归地继续探索这个新瓷砖。
使用一个二维布尔数组来记录每个瓷砖是否已经被访问过,以防止重复计数或无限循环。
计数:

使用一个全局变量或者传递一个引用/指针参数来记录已经访问过的黑色瓷砖数量。
输出结果: 每完成一组数据的DFS后,输出已经访问过的黑色瓷砖数量。

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

int w, h;
vector<vector<char> > a;
vector<vector<bool> > vis;
int ***t;

int fx[4] = {0, 0, -1, 1};
int fy[4] = {-1, 1, 0, 0};

void dfs(int x, int y){
    vis[y][x] = true;
    ***t++;
    for (int i = 0; i < 4; i++){
        int tx = x + fx[i];
        int ty = y + fy[i];
        if (tx >= 0 && tx < w && ty >= 0 && ty < h && a[ty][tx] == '.' && !vis[ty][tx]){
            dfs(tx, ty);                
        }
    }
}

int main(){
    while (cin >> w >> h && (w || h)){
        a.assign(h, vector<char>(w));
        vis.assign(h, vector<bool>(w, false));
        ***t = 0;
        int s1 = -1, s2 = -1;
        for (int i = 0; i < h; i++){
            for (int j = 0; j < w; j++){
                cin >> a[i][j];
                if (a[i][j] == '@'){
                    s1 = j;
                    s2 = i;
                }
            }
        }       
        if (s1 != -1 && s2 != -1 && a[s2][s1] == '@'){
            dfs(s1, s2);
        }
        cout << ***t << endl;
    }
    return 0;
} 

1217:棋盘问题

回溯!!!

思路

1、遍历棋盘:从棋盘的第一行开始,尝试在每个允许的位置(即#表示的位置)放置一个棋子。
2、检查冲突:在放置每个棋子之前,检查当前列以及之前的行中是否已经放置了棋子,因为棋子不能放在同一行或同一列。
3、递归:对于每个允许放置棋子的位置,放置一个棋子后,递归地尝试在剩下的行中放置剩余的棋子。
4、回溯:如果当前行没有合法的位置可以放置棋子或已经放置了所有棋子,回溯到上一步,即移除当前行的棋子,尝试下一个可能的位置。
5、计数:每当成功地在棋盘上放置了k个棋子,就增加一种方案。

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

int n, k, ans;
vector<string> b;
vector<bool> vis;

void dfs(int r, int p){
	if (p == k){	//判断特殊情况 
		ans++;
		return;		//结束函数 
	}
	if (r >= n) return;
	for (int i = 0; i < n; i++){
		if (b[r][i] == '#' && !vis[i]){
			vis[i] = true;
			dfs(r + 1, p + 1);
			vis[i] = false;		//回溯,撤销上一步 
		}
	}
	dfs(r + 1, p); 
}

int main(){
	while (cin >> n >> k && n != -1 && k != -1){
		b.resize(n);
		for (int i = 0; i < n; i++){
			cin >> b[i];
		}
		vis.resize(n, false);
		ans = 0;
		dfs(0, 0);
		cout << ans << endl;
	}
	return 0;
}

1218:取石子游戏

我认为比较简单

大致思路:
基本逻辑:根据题目描述,我们需要判断在给定的两堆石子数量下,先手是否能够获胜。这可以通过递归地模拟取石子的过程来实现,每次递归时交换玩家的角色。
递归终止条件:如果当前状态下,一堆石子的数量是另一堆的两倍或更多,则当前操作的玩家赢。如果两堆石子中的任何一堆数量为0,则当前操作的玩家输。
搜索过程:从较多的那堆石子中取出一定数量的石子,数量是较少那堆的整数倍,然后递归地调用函数模拟下一步操作。
优化:考虑到可能存在重复状态,我们可以使用记忆化搜索来避免重复计算已经计算过的状态。
#include <iostream>
using namespace std;

bool dfs(int a, int b){
	if (a < b) swap(a, b);
	if (a % b == 0) return true;
	if (a / b >= 2) return true;
	return !dfs(a - b, b);
}

int main(){
	int a, b;
	while (cin >> a >> b && (a || b)){
		if (dfs(a, b)) cout << "win" << endl;
		else cout << "lose" << endl;
	}
	return 0;
}

1219:马走日

这里是解决这个问题的基本步骤:

1、初始化棋盘:创建一个n×m的棋盘,所有格子初始标记为未访问。

2、马的移动规则:确定马能移动的8个方向。在中国象棋中,马的移动可以用坐标变化来表示,例如:(2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (1, -2), (-1, 2), (-1, -2)。

3、回溯搜索:从初始位置开始,尝试每一种可能的移动。如果移动有效(即移动后的位置在棋盘内且未被访问过),则继续从新位置递归搜索。

4、统计途径总数:每当成功访问到棋盘上的所有点时,途径总数加一。需要注意的是,每访问一个新点,就标记为已访问,回溯后需要撤销这个标记。

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

//方向数组——马可能走的8个方向 
int fx[8] = {2, 2, -2, -2, 1, 1, -1, -1};
int fy[8] = {1, -1, 1, -1, 2, -2, 2, -2};
int n, m, ***t;

void dfs(vector<vector<bool> >& vis, int x, int y, int step){
	if (step == n * m){		//特殊条件,所有的点都已经访问过了 
		***t++;
		return;
	}
	//遍历8个方向 
	for (int i = 0; i < 8; i++){
		int tx = x + fx[i];
		int ty = y + fy[i];
		if (tx >= 0 && tx < n && ty >= 0 && ty < m && !vis[tx][ty]){
			vis[tx][ty] = true;
			dfs(vis, tx, ty, step + 1);		//递归
			vis[tx][ty] = false;			//回溯,撤销上一步的标记	
		}
	}	
}

int main(int argv, char* argc[]){
	int t, x, y;
	cin >> t;
	while (t--){
		cin >> n >> m >> x >> y;
		vector<vector<bool> > vis;
		vis.assign(n, vector<bool>(m, false));
		***t = 0;
		vis[x][y] = true;
		dfs(vis, x, y, 1);	//深搜
		cout << ***t << endl; 
	}
	return 0;
} 

这段代码首先定义了一个全局变量***t来统计马遍历棋盘的途径总数。dfs函数是核心函数,它尝试所有可能的移动并递归地搜索整个棋盘。每次成功遍历整个棋盘时,***t会增加。最后,输出每组数据的途径总数。

1220:单词接龙


1221:分成互质组


1222:放苹果


还剩几道题,以后需更!!!

转载请说明出处内容投诉
CSS教程_站长资源网 » 信息学奥赛一本通之基础算法——第五章 搜索与回溯算法

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买