搜索技巧之-剪枝

浪矢\n / 2023-07-22 / 原文

剪枝是去除搜索树当中不必要的搜索路径,从而优化算法,降低时间开销。

常见的剪枝包括:

1可行性剪枝   2排除等效剪枝   3最优性剪枝    4顺序剪枝     5记忆化剪枝

下面将一一举例介绍其原理:

1可行性剪枝

在寻找所有的解决方案时,若某种方案明显不可行/无法找到答案,则停止继续搜索。

2排除等效剪枝

当该方案和之前搜过的方案对结果具有相同效果时,可以只使用一个继续向下搜索。

3最优性剪枝

当前的方案与已有方案相比,结果明显不是最优的时候,例如在某一层取max/min后,发现不能得到更优解时,停止继续搜索。

4顺序剪枝

似乎不太常见,在搜索之前进行排序(预处理),搜索过程中可以结合其他方法进行剪枝。

5记忆化剪枝

当每个状态的结果是唯一确定且有可能会多次重复搜索时,记录每个状态的搜索结果,并在再次调用时直接返回记录值。

下面将是一些例题:

 

洛谷  P1731  生日蛋糕

从下往上搜索,枚举每层的半径和高度作为分支

搜索状态:第dep层,当前外表面积s,当前体积v,dep+1层的高度和半径

整个蛋糕的底面积=最底层的圆面积,这样在m-1层搜索时,只需要计算侧面积

剪枝:

1.是否可行剪枝

首先枚举,其次枚举

2.优化搜索顺序

倒序搜索枚举

3.可行性剪枝
预处理最小体积和侧面积,然后剪枝

可行剪枝

#include <bits/stdc++.h>
using namespace std;
int n,m;
int ans=2e9;
void dfs(int dep,int s,int v,int past_r,int past_h)
{
    if(s >= ans) return;
    if(dep == m+1 && v == n)
    {
        ans = min(ans,s);
        return;
    }
    if(v >= n) return;
    int rest_dep = m - dep + 1;
    if(rest_dep * past_r * past_r * past_h + v < n) return;
    if(dep == 1)
    {
        for(int r = past_r; r >= m; --r)
            for(int h = m; h <= past_h; ++h)
                dfs(dep + 1,s + r * r + 2 * r * h,v + r * r * h,r,h);
    }
    else
    {
        for(int r=past_r-1; r>=m-dep+1; --r)
            for(int h=m-dep+1; h<past_h; ++h)
                dfs(dep + 1,s + 2 * r * h,v + r * r * h,r,h);
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    dfs(1,0,0,28,28);
    if(ans == 2e9) printf("0\n");
    else printf("%d\n",ans);
}

 

洛谷  P1120 小木棍

剪枝

1.优化搜索顺序:从大到小排序,优先尝试长的木棍

2.排除等效分支:

(1).因为先拼一根a长度的,再拼一根b长度的等于先拼一根b长度的,再拼一根a长度,只要搜索其中一种

(2).记录最近一次尝试拼接的木棍长度,如果搜索失败则不尝试同种长度的木棍

(3).当尝试拼入的第一根木棍就返回失败,则直接回溯

(4).如果当前木棍拼入后,一节木棒被填充完整,而另一根木棍

失败了,则直接回溯

剪枝代码
 #include<bits/stdc++.h>
#define MAXN 100005
using namespace std;
int n,sum=0,a[MAXN];
bool vis[MAXN]; 
int cmp(int a,int b)
{
    return a>b;
}
bool dfs(int num,int len,int now_len,int number)
{
    if(number==sum/len)return 1;
    if(now_len==len)
    {
        if(dfs(1,len,0,number+1))return 1;
    }
    for(int i=num; i<=n; i++)
    {
        if(!vis[i]&&now_len+a[i]<=len)
        {
            vis[i]=true;
            if(dfs(i+1,len,now_len+a[i],number))return 1;
            vis[i]=false;
            if(a[i]==len-now_len||now_len==0)break;
            while(a[i]==a[i+1])i++; 
        }
    }
    return 0;
}
int main()
{
    memset(vis,false,sizeof(vis));
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&a[i]);
        sum+=a[i];
    }
    sort(a+1,a+1+n,cmp);
    for(int i=a[1]; i<=sum; i++)
    {
        if(sum%i!=0)continue;
        if(dfs(1,i,0,0))
        {
            cout<<i<<endl;
            break;
        }
    }
}
 

洛谷吃奶酪

我们只需要记录当前走过的距离来和ans做比较,重点讲一下如何将路径用二进制压缩。
首先我们在状态中有一个 当前到了数组哪个点的状态,命名为nowdot。每次定义p为当前点到第i个的距离,用二进制保存,初始化为int p = nowdot + (1 << (i - 1)),如果从p到i的距离已经有过更新并且本次构造的距离还大于之前,则跳过;否则更新,继续搜索。

状压+剪枝
 #include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;

//dfs(当前吃了多少个,当前吃的第几个,当前跑了多远,现在的点)
const int N = 20;
int n;
bool vis[N];
double ans = 1e9, dp[65000][20]; //dp用来存路径 
struct node {
	double x;
	double y;
}a[N];

double discal (int l, int m) { //距离计算函数 
	return sqrt((a[l].x - a[m].x) * (a[l].x - a[m].x) + (a[l].y - a[m].y) * (a[l].y - a[m].y)); 
}

void dfs(int sum, int nowi, double dis, int nowdot) {
	if (dis > ans) {
		return;
	} //剪枝 
	if (sum == n) {
		if (dis < ans) ans = dis;
		return;
	}
	for (int i = 1; i <= n; i++) {
		if (!vis[i]) {
			int p = nowdot + (1 << (i - 1)); 
			//二进制状态压缩从当前点到这个点 
			if (dp[p][i] != 0 && dp[p][i] <= dis + discal(nowi, i)) {
				continue;
			} 
			vis[i] = true;
			dp[p][i] = dis + discal(nowi, i);
			dfs(sum + 1, i, dp[p][i], p);
			vis[i] = false;
		}
	}
}
int main() {
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i].x >> a[i].y;
	}
	dfs(0, 0, 0, 0);
	printf("%.2lf", ans);
	return 0;
}

 

P1434 [SHOI2002] 滑雪

记忆化
 #include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int dx[4]={0,0,1,-1};
int dy[4]={1,-1,0,0};
int n,m,a[201][201],s[201][201],ans;
bool use[201][201];//这个就是所谓的不需要
int dfs(int x,int y){
    if(s[x][y])return s[x][y];//记忆化搜索
    s[x][y]=1;//题目中答案是有包含这个点的
    for(int i=0;i<4;i++)
    {  int xx=dx[i]+x;
       int yy=dy[i]+y;//四个方向
       if(xx>0&&yy>0&&xx<=n&&yy<=m&&a[x][y]>a[xx][yy]){
       	  dfs(xx,yy);
          s[x][y]=max(s[x][y],s[xx][yy]+1);
       }
    }
    return s[x][y];
}
int main()
{	
   scanf("%d%d",&n,&m);//同题目的R,C
   for(int i=1;i<=n;i++)
     for(int j=1;j<=m;j++)
       scanf("%d",&a[i][j]);
    for(int i=1;i<=n;i++)//找从每个出发的最长距离
      for(int j=1;j<=m;j++)
        ans=max(ans,dfs(i,j));//取最大值
    printf("%d",ans);
    return 0;
}