算法——回溯与剪枝(Backtracking and pruning)

算法之回溯与剪枝(Backtracking and pruning)

  • 思想:将回溯法与分支限界法原理结合、应用访问,用剪枝来排除不满足解的情况来提高算法的执行效率
  • 算法总结:回溯法的优点是可以遍历所有的解的空间,容易编程,也可以在遍历的过程中除去不满足解空间的路径,大大的减少了深度遍历所带来的资源的巨大消耗。
回溯法的步骤:
1.找出能解决问题的所有的解
2.用递归的思想来一个解一个解的进行遍历和计算
3.在计算各个解的过程中根据要求,记录题目要求的解
4.对不满足解的路径进行剔除
5.最后回溯到起点

下面是一些例题,可以帮助大家理解和学习回溯键值的算法思想

  1. 走迷宫

在一个 m×n 的迷宫里,从起点开始,依次按东(右)、南(下)、西(左)、北(上) 4 个方向探索通路,直至达到终点为止。迷宫由字符组成,W表示墙,. 表示空地,请编写程序,输出你找到的首条通道。
思路:从起点时开始出发依次按照右下左上的顺序进行深度探索(用一个数组来存储依次遍历的顺序),如果在遍历的过程中出现坐标越界或者进入“死胡同”(其它的三个方向都是墙)那么就回退到上一个位置,再继续探索下一个位置,直到走到迷宫出口。
样例1:
5 7
W W . W W W W
W . . . W . .
W . W W W . W
W . W . . . W
W W W . W W W
0 2
4 3
输出:
None
样例2:
5 7
W W . W W W W
W . . . W . .
W . W W W . W
W . . . W . W
W W W . W W W
0 2
4 3
输出:
W W * W W W W
W * * o W . .
W * W W W . W
W * * * W . W
W W W * W W W
代码如下:

#include<bits/stdc++.h>
using namespace std;
#define check(i,j,m,n) i<1||j<1||i>m||j>n

const int ma=1000;
char my_place[ma][ma];
int is_cross[ma][ma];
int m,n,x,y,x2,y2,t;
int my_move[4][2]={{0,1},{1,0},{0,-1},{-1,0}};

void walk(int i,int j);
bool no_path(int i,int j);

int main(){
    cin>>m>>n;
    for(int i=1;i<=m;i++)
        for(int j=1;j<=n;j++)
            cin>>my_place[i][j];
    cin>>x>>y>>x2>>y2;
    x+=1;y+=1;x2+=1;y2+=1;
    walk(x,y);
    if(t==0) cout <<"None"<<endl;
    else{
        for(int i=1;i<=m;i++){
            for(int j=1;j<=n;j++)
                if(j==1) cout<<my_place[i][j];
                else cout<<" "<<my_place[i][j];
            cout<<endl;
        }
    }
    return 0;
}

bool no_path(int i,int j){
  if(is_cross[i-1][j]==1&&my_place[i][j+1]=='W'&&my_place[i+1][j]=='W'&&my_place[i][j-1]=='W')
      return true;
  else if(is_cross[i][j+1]==1&&my_place[i+1][j]=='W'&&my_place[i][j-1]=='W'&&my_place[i-1][j]=='W')
      return true;
  else if(is_cross[i+1][j]==1&&my_place[i][j+1]=='W'&&my_place[i-1][j]=='W'&&my_place[i][j-1]=='W')
      return true;
  else if(is_cross[i][j-1]==1&&my_place[i-1][j]=='W'&&my_place[i][j+1]=='W'&&my_place[i+1][j]=='W')
      return true;
  return false;
}
void walk(int i,int j){
    if(i==x2&&j==y2){
        is_cross[i][j]=1;
        my_place[i][j]='*';
        t=1;
        return;
    }
    if(no_path(i,j)) {
        my_place[i][j]='o';
        return;
    }
    for(int j1=0;j1<4;j1++){
        int newx=i+my_move[j1][0];int newy=j+my_move[j1][1];
        if(t==1||check(newx,newy,m,n)) return;
        if(my_place[newx][newy]!='W'&&is_cross[newx][newy]==0) {
            is_cross[i][j]=1;
            my_place[i][j]='*';
            walk(newx,newy);
        }
    }
}

  1. 0/1背包问题

0/1背包问题。给定一载重量为W的背包及n个重量为wi、价值为vi的物体,1≤i≤n,要求而且重量和恰好为W具有最大的价值。
思路:这道题本质是全排列的问题,即罗列出所有的可能的选择的组合,然后计算出每个组合的价值,最后求出最大的价值即可。
输入样例1:
5 10
2 6
2 3
6 5
5 4
4 6
输出样例1:
1 2 5
15
输入样例2:
2 10
11 2
13 100
输出样例2:
No
0
代码如下:

// Created by Chenglong Shi on 2021/11/3.
// Only can use to study
// Once found commercial or illegal use will be pursued to the end
// Banning plagiarism
// Email:2230307855@qq.com
// 内部可能含有拼音和汉语注释
// by 史成龙
// 方法:
//
#include<bits/stdc++.h>
using namespace std;

const int ma=50;

struct goods{
    int weight,space;
    goods(){weight=space=0;}
};

goods things[ma];
int path[ma];
int dp[ma][ma];
int w,n,top,best;

void put_goods();
void get_path();

int main(){
    cin>>n>>w;
    for(int i=1;i<=n;i++) cin>>things[i].space>>things[i].weight;
    put_goods();
    best=dp[n][w];
    get_path();
    if(!best){
        cout<<"No"<<endl;
        cout<<"0";
    }
    else{
        for(int i=top-1;i>=0;i--)
            cout<<path[i]<<" ";
        cout<<"\n"<<best;
    }
    return 0;
}

void put_goods(){
    for(int i=1;i<=n;i++){
        for(int j=1;j<=w;j++){
            if(things[i].space>j) dp[i][j]=dp[i-1][j];
            else
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-things[i].space]+things[i].weight);
        }
    }
}
void get_path(){
    int k=w;
    for(int i=n;i>=1;i--) {
        if (dp[i][k] == (dp[i - 1][k - things[i].space] + things[i].weight)) {
            path[top++] = i;
            k = w - things[i].space;
        }
    }
}

  1. 集合的划分

集合的划分
思路:这是一个贝尔数的应用例子,
1
1 2
2 3 5
5 5 8 13….
用数组进行存储a[n][n]就是要求的解
输入样例:
5
输出样例:
52
代码如下:

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

const int ma=5e3+1;

int bell[ma][ma];
int n;

void init();

int main(){
	cin>>n;
	init();//打表
	cout<<bell[n][n]; 
	return 0;
}

void init(){
  bell[1][1]=1; bell[2][1]=1; bell[2][2]=2;
  bell[3][1]=2; bell[3][2]=3; bell[3][3]=5;
  for(int i=4;i<=n;i++){
  	int begin=bell[i-1][i-1];
  	int be=0,en=0;
  	for(int j=1;j<=i;j++){
  		if(j==1) {
  			bell[i][j]=begin;
  			be=begin;
		}
		else if(j==i){
			bell[i][j]=be+en;
		}
  		else{
  		    bell[i][j]=bell[i][j-1]+bell[i-1][j-1];
			if(j==(i-1)){
				en=bell[i][j];
			}	
	    }
	  }
  }
}

  1. 半数集

给定一个自然数n,由n 开始可以依次产生半数集set(n)中的数如下(注意半数集是多重集)。
1. n∈set(n);
2. 在n 的左边加上一个自然数,但该自然数不能超过最近添加的数的一半;
3. 按此规则进行处理,直到不能再添加自然数为止。
例如,set(6)={6,16,26,126,36,136}。半数集set(6)中有6 个元素。

思路:每一个数的半数集都与前面的数到此数的一半相对应,然后加上1就是当前数的半数集的个数
输入样例:
6
输出样例:
6
代码如下:

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

int n;
const int ma=61;
int key[ma];

void init();//打表计算

int main(){
    init();
    cin>>n;
    cout<<key[n];
	return 0;
}

void init(){
	key[1]=1;
	for(int i=2;i<=ma;i++){
		for(int j=1;j<=i/2;j++)
		   key[i]+=key[j];
		key[i]++;
	}
}

  1. 排列问题

(0<m≤26) 个大写字母中任意选出 n (0<n≤m) 个字母排成一行,一共有多少种排列?请编写程序,输入 m 和 n,输出从 A 开始的连续 m 个字母中任取 n 个字母的所有排列。
要求:每行输出一个排列,按字典序输出.

思路:先用char数组存储A到N的数据,然后用一个全局变量来表示当前读到第几个字母,如果读到N就回退再读其它的方案,直到所有的方案读取完毕。
输入样例:
3 2
输出样例:
AB
AC
BA
BC
CA
CB
代码如下:

// Created by Chenglong Shi on 2021/10/31.
// Only can use to study
// Once found commercial or illegal use will be pursued to the end
// Banning plagiarism
// Email:2230307855@qq.com
// 内部可能含有拼音和汉语注释
// by 史成龙
// 方法:回溯+全局变量控制
//
#include<stdio.h>

const int ma=30;
int vis[ma],m,n,my_index=0;
char val[ma];
char re[ma];

void show_permutation(int num);
void show_result();

int main(){
    scanf("%d %d",&m,&n);
    for(int i=65;i<(65+m);i++) val[i-65]=(char)i;
    show_permutation(0);
    return 0;
}

void show_result(){
    for(int i=0;i<n;i++) printf("%c",re[i]);
    printf("\n");
}
void show_permutation(int num){
    if(num>=n){
        show_result();
        return;
    }
    for(int i=0;i<m;i++){
        if(!vis[i]){
            re[my_index++]=val[i];
            vis[i]=1;
            show_permutation(num+1);
            vis[i]=0;
            my_index--;
        }
    }
}

  • 希望以上知识可以对大家的学习有所帮助哈,这一部分挺难的,大家加油呀!👍
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
回溯算法是一种通过不断地尝试所有可能的解决方案来解决问题的算法。它通常用于解决组合、排列、子集、棋盘等问题。回溯算法的基本思想是在每一步尝试所有可能的解决方案,如果当前方案无法解决问题,则回溯到上一步并尝试其他方案,直到找到解决方案或者所有可能的方案都被尝试过。 剪枝操作是回溯算法中的一种优化技巧,它可以减少搜索空间,从而提高算法的效率。剪枝操作通常在搜索过程中进行,通过判断当前搜索路径是否有可能找到解决方案,来决定是否继续搜索。常见的剪枝操作包括:去重剪枝、可行性剪枝、最优性剪枝等。 下面是Java实现回溯算法及其剪枝操作的代码示例: 1. 未优化的回溯算法代码: ``` class Solution { List<List<Integer>> lists = new ArrayList<>(); List<Integer> path = new ArrayList<>(); public List<List<Integer>> combine(int n, int k) { backTracking(n, k, 1); return lists; } public void backTracking(int n, int k, int startIndex){ if(k == path.size()){ lists.add(new ArrayList<>(path)); return; } for(int i = startIndex; i <= n; i++){ path.add(i); backTracking(n, k, i + 1); path.remove(path.size() - 1); } } } ``` 2. 去重剪枝优化的回溯算法代码: ``` class Solution { List<List<Integer>> lists = new ArrayList<>(); List<Integer> path = new ArrayList<>(); public List<List<Integer>> combine(int n, int k) { backTracking(n, k, 1); return lists; } public void backTracking(int n, int k, int startIndex){ if(k == path.size()){ lists.add(new ArrayList<>(path)); return; } for(int i = startIndex; i <= n - (k - path.size()) + 1; i++){ path.add(i); backTracking(n, k, i + 1); path.remove(path.size() - 1); } } } ``` 在上面的代码中,我们使用了去重剪枝来避免重复搜索相同的解决方案。具体来说,我们在搜索过程中,限制了每个数字只能出现一次,从而避免了重复搜索相同的解决方案。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SweetCode

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值