[AcWing算法提高课]之搜索 DFS剪枝与优化(C++题解)

目录

DFS剪枝与优化

0)剪枝优化策略

1)小猫爬山

2)数独

3)木棒

4)生日蛋糕(数学剪枝!!!)


DFS剪枝与优化

0)剪枝优化策略

来自yxc算法提高课 的大佬:深度优先搜索(DFS)的剪枝方式 - AcWing

[AcWing算法提高课]之搜索 DFS剪枝与优化(C++题解)_第1张图片

1)小猫爬山

[AcWing算法提高课]之搜索 DFS剪枝与优化(C++题解)_第2张图片

 [AcWing算法提高课]之搜索 DFS剪枝与优化(C++题解)_第3张图片

 输入概要

第一行为 小猫的总数n   缆车的最大承受重量

接下来的n行为 :各个小猫的重量

核心思路

  1. 暴力搜索dfs
  2. 对于每一个小猫,有两种选择,让它上这辆缆车,或这不让它上这辆缆车
  3. 如果枚举到了最后一支小猫,那么此时就记录下缆车数量的最小值 即可

剪枝优化:

  1. 优化搜索顺序:将小猫按重量从大到小排序进行搜索,这样可以减少分支
  2. 可行性剪枝:判断小猫上这辆缆车与否,关键是要看 缆车上已有小猫的重量+当前枚举小猫的数量是否大于给定的限制的重量,如果是小于等于给定的限制重量,那么就可以将该小猫上车,如果是大于给定的限制的重量,那么就将这个小猫安置在新开的缆车上
  3. 最优化剪枝:如果当前枚举的缆车数量 已经 大于等于 之前得到的“最小”缆车数量 那么就可以提前剪枝并返回,避免没有必要的搜索

完整代码:

#include 
#include 

using namespace std;

const int N = 2e1;

int cat[N], cab[N];
int n, w;
int ans;

bool cmp(int a, int b) 
{
    return a > b;
}

void dfs(int now, int cnt)//当前遍历到的小猫编号 ,当前开的缆车个数
{
    if (cnt >= ans) {
        return;
    }
    if (now == n + 1) 
    {
        ans = min(ans, cnt);
        return;
    }
    //尝试分配到已经租用的缆车上
    for (int i = 1; i <= cnt; i++) 
    {  //分配到已租用缆车
        if (cab[i] + cat[now] <= w) 
        {
            cab[i] += cat[now];
            dfs(now + 1, cnt);
            cab[i] -= cat[now];  //还原
        }
    }

    // 新开一辆缆车
    cab[cnt + 1] = cat[now];
    dfs(now + 1, cnt + 1);
    cab[cnt + 1] = 0;
}

int main() 
{
    cin >> n >> w;
    for (int i = 1; i <= n; i++) cin >> cat[i];

    sort(cat + 1, cat + 1 + n, cmp);

    ans = n;

    dfs(1, 0);

    cout << ans << endl;

    return 0;
}

2)数独

[AcWing算法提高课]之搜索 DFS剪枝与优化(C++题解)_第4张图片

 [AcWing算法提高课]之搜索 DFS剪枝与优化(C++题解)_第5张图片

 核心思路:

  1. 爆搜,但这个好像是废话
  2. 对于每一个格子,依次枚举其合理数字的情况 ,重复此操作
  3. 若对于(2)操作时 ,有冲突错误,证明 某一步发生错误 进行回溯判断,因此证明了我们的爆搜函数 是判断函数bool
  4. 那么当格子被填满且没有发生错误,那么此时就是一种正确情况,此时就正解

剪枝优化::

毫无疑问:上述的这个步骤会超时,因此需要剪枝优化

  1. 优化剪枝顺序:对于一个‘.’  就是我们要填入的数字,那么我们希望它的数字的可能种类较少,这样我们枚举的分支才会减少,所以先从数字的可能种类少的开始枚举
  2. 预处理优化判断:(1)中说到,要判断一个 ' . ' 的数字的可能种类,那么数独的规则就是每一行,每一列,每一个3*3的宫格内的数字不重复即可,所以我们要记录每一行,每一列,一个3*3的宫格内数字的种类,直观的想法是用数组存对应的数字
  3. 预处理优化判断:对于(2)而言,存了数字,那么还要寻找不包含这三个 限制的所有数字,其实就很冗余了,那么这个时候可以 用二进制优化,举个例子:
    对于一个未填数字的'.'处 坐标为(x,y)
    它的行上      已经 有了 2 4 7 这三个数字
    它的列上      已经 有了 2 3 9 这三个数字
    它的3*3宫格上 已经 有了 1 2 5 这三个数字
    
          9 8 7 6 5 4 3 2 1 0
    高位--------->>>>>>>>>低位
            row[x]=1101101011
            col[y]=0111110011
    ceil[x/3][y/3]=1111011001
    
                   0101000001
    0表示这个点上有数字
    将它们按位 & 的结果是:
    所以此时可选的数字只有 8 和 6  因为数独中数字的范围是1~9(所以二进制的0位去掉)
    
    

 因此对于这个二进制优化,我们又得下一番功夫......

二进制优化

  1. 求出1~2^9次方  中  数字对应 数字'1'出现的个数
  2. 然后写一个判断函数 ,判断行 + 列 + 宫格 中 1 的个数
  3. 因为要枚举1的个数,所有要快速 知道 二进制中的 '1' 出现在 哪 一个位置上,这里使用算法基础课的lowbit 函数,O(1)时间内求出 pow(2,最后一位1的位置)的数值大小
  4. 因为这个(3)所以,我们要预处理 log2 M 对应的数,这样才能得到我们想要得到的位置对对应的数字 ,用map数组进行存储
  5. 初始化:因为这道题有多组输入,所以一开始在没有输入数字之前,所有的点 都可以选择 1~9的数字,也就是每一位上都是1(除了第0位)(因此要-1)

好了,这道题,其实都是预处理剪枝,等效冗余的情况下采取了,二进制优化,分析好了,那么码代码就不是什么难事了

#include 
#include 
#include 

using namespace std;

const int N = 9, M = 1 << N;

int ones[M], map[M];
int row[N], col[N], cell[3][3];
char str[100];

void init()//初始状态每一位除了第一位 都是1  -->>因为第一位为0 数独中不为0
{
    for (int i = 0; i < N; i ++ )
        row[i] = col[i] = (1 << N) - 1;

    for (int i = 0; i < 3; i ++ )
        for (int j = 0; j < 3; j ++ )
            cell[i][j] = (1 << N) - 1;
}

void draw(int x, int y, int t, bool is_set)//在(x,y)坐标上放入数字t
{
    if (is_set) str[x * N + y] = '1' + t;
    else str[x * N + y] = '.';

    int v = 1 << t;//转为二进制
    if (!is_set) v = -v;

    row[x] -= v;//行的二进制该数字减去这个1
    col[y] -= v;//列的二进制该数字进去这个1
    cell[x / 3][y / 3] -= v;//同理
}

int lowbit(int x)//返回  pow(2,最后一位1的位置)
{
    return x & -x;
}

int get(int x, int y)//计算可枚举的数字
{
    return row[x] & col[y] & cell[x / 3][y / 3];
}

bool dfs(int cnt)//空格的数量
{
    if (!cnt) return true;//没有空格了,证明已经被填满了-->>return true

    int minv = 10;
    int x, y;
    for (int i = 0; i < N; i ++ )//找到可填写最少种类数字的空格
        for (int j = 0; j < N; j ++ )
            if (str[i * N + j] == '.')//该位置该填数字
            {
                int state = get(i, j);//接受
                if (ones[state] < minv)
                {
                    minv = ones[state];//ones[state]计算该数字的1的个数 -->>也就是可填写多少个数字
                    x = i, y = j;//记录位置
                }
            }

    int state = get(x, y);
    for (int i = state; i; i -= lowbit(i))
    {
        int t = map[lowbit(i)];//最后一个1的位置:也就是枚举到的该数字:1~9
        draw(x, y, t, true);//放入
        if (dfs(cnt - 1)) return true;
        draw(x, y, t, false);
    }

    return false;
}

int main()
{
    map存的是  pow(i,2)==i  -->>也就是快速得到一个数	M 的log2(M)的结果
    for (int i = 0; i < N; i ++ ) map[1 << i] = i;//预处理
    
    for (int i = 0; i < 1 << N; i ++ )//枚举0~2^9次方的数字中1的个数
        for (int j = 0; j < N; j ++ )//枚举宫格中一个数的9个位数
            ones[i] += i >> j & 1;//计算该数中在二进制下 有多少个1

    while (cin >> str, str[0] != 'e')
    {
        init();//初始化

        int cnt = 0;
        for (int i = 0, k = 0; i < N; i ++ )
            for (int j = 0; j < N; j ++, k ++ )
                if (str[k] != '.')
                {
                    int t = str[k] - '1';//已经填好的数字
                    draw(i, j, t, true);
                }
                else cnt ++ ;//. 说明要填入数字,cnt:有多少个空的待填入数字

        dfs(cnt);

        puts(str);
    }

    return 0;
}

3)木棒

[AcWing算法提高课]之搜索 DFS剪枝与优化(C++题解)_第6张图片

 [AcWing算法提高课]之搜索 DFS剪枝与优化(C++题解)_第7张图片

 样例解释:

第一行输入: 小木棍的个数

第二行输入:小木棍的长度

输出:木棒的最小长度(注意是等长的木棒)

对于第一组样例:

(5+1)        (5+1)        (5+1)        (2+2+2)  木棒的最小长度为 6

对于第二组样例:

(1+4)         (2+3)  木棒的最小长度为:5

核心思路:

大木棒的总长度==等长的木棒拼接而成

参考自:AcWing 167. 木棒(详细证明,图解版) - AcWing

  1. 找到不变的量:砍之前 大木棒的总长度==木棒*木棒的数量,对于这个等式而言,要使得木棒的长度最小,大木棒的总长度又不能改变,那么只需增加木棒的数量,并记录木棒的长度增加
  2. 剩下的就是小猫爬山一样的整体思路,将木棍进行分组,如果组已有的长度+当前遍历到的木棍的长度>限定的长度,那么就将该木棍加到一个新的分组中

剪枝优化:

  1. 剪枝顺序优化:这个问题中木棍的长度跟搭配的顺序没有关系,这个是显然的
  2. 剪枝顺序优化:从木棍长度长的一端开始枚举,这样会使后序的分支减少
  3. 剪枝等效冗余:如果第i跟棍子失败了,那么第i+1根棍子与第i根棍子一样长,那么也是会失败的,显然正确

 这个代码说实话,还是很难写的(对我目前来说)

#include
#include
#include
using namespace std;

const int N = 70;
int w[N], sum, length, n;
bool st[N];

//第u组,part第u组的已有长度,start表示第u组的枚举位置;
bool dfs(int u, int part, int start)
{
	if (u * length == sum) return true;//如果总长度已经达到了,return true
	if (part == length) return dfs(u + 1, 0, 0);//下一组

	for (int i = start; i <= n; i++)
	{
		if (st[i]) continue;//已经用过了
		if (w[i] + part > length) continue;//原来的+遍历到的>大于该组长度
		st[i] = true;//标记已经使用

		if (dfs(u, w[i] + part, i + 1)) return true; //因为前i个棍子都在第u组枚举了, 所以直接从第i + 1根棍子开始枚举
		st[i] = false;//返回上层分支时要恢复现场(枚举该组不选择第i根根子的方案)

		if (!part || w[i] + part == length) return false;//如果第一根失败了或者最后一根失败了,就一定失败

		int j = i;//如果i失败了,那么长度跟i一样的棍子也一定失败
		while (j <= n && w[j] == w[i]) j++;
		i = j - 1;
	}
	return false;//枚举完了还没有成功,就返回失败
}

int main()
{
	while (cin >> n,n)
	{
		memset(st, 0, sizeof st);
		sum = 0, length = 1;//sum:所有大木棍的总和,length:单根大木棍的长度

		for (int i = 1; i <= n; i++)
		{
			cin >> w[i];
			sum += w[i];//总和
		}
		
		//倒着排序,减少分支
		sort(w + 1, w + n + 1);
		reverse(w + 1, w + n + 1);

		while (1)//枚举length的长度
		{
			if (sum % length == 0 && dfs(0, 0, 0))
			{
				cout << length << endl;
				break;
			}
			length++;
		}
	}

	return 0;
}

4)生日蛋糕(数学剪枝!!!)

[AcWing算法提高课]之搜索 DFS剪枝与优化(C++题解)_第8张图片

#include
#include
using namespace std;

const int N = 24, INF = 1e9;

int n, m;
int minv[N], mins[N];
int res = INF;

//记录每层的半径和高,因为会用到上一层的高度
int R[N], H[N];

//u当前层次,v当前处理的体积和,s当前处理的面积和
void dfs(int u, int v, int s)
{
    if(v + minv[u] > n) return;
    if(s + mins[u] >= res) return;
    if (s + 2 * (n - v) / R[u + 1] >= res) return;

    if(!u)
    {
        if(v == n) res = s;
        return;
    }

    //搜索顺序,先R后H,从大到小
    for(int r = min(R[u + 1] - 1,(int)sqrt((n - v - minv[u - 1]) / u)); r >= u; r--)
        for(int h = min(H[u + 1] - 1, (n - v - minv[u - 1]) / r / r); h >= u; h--)
        {
            H[u] = h, R[u] = r;

            //最底层的时候需要加上r*r
            int t = u == m ? r * r : 0;

            dfs(u - 1, v + r * r * h, s + 2 * r * h + t);
        }
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        minv[i] = minv[i - 1] + i * i * i;
        mins[i] = mins[i - 1] + 2 * i * i;
    }

    //m+1层不存在,作为哨兵,减少边界情况的判断
    R[m + 1] = H[m + 1] = INF;

    dfs(m, 0, 0);

    if(res == INF) res = 0;
    cout << res << endl;


    return 0;
}

作者:random_srand
链接:https://www.acwing.com/solution/content/31876/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 样例解释:

 题解参考自:AcWing 168. 生日蛋糕【图解+推导】 - AcWing

原谅我:这个等我有空的时候再写()  要补高数作业了

#include
#include
using namespace std;

const int N = 24, INF = 1e9;

int n, m;
int minv[N], mins[N];
int res = INF;

//记录每层的半径和高,因为会用到上一层的高度
int R[N], H[N];

//u当前层次,v当前处理的体积和,s当前处理的面积和
void dfs(int u, int v, int s)
{
    if(v + minv[u] > n) return;
    if(s + mins[u] >= res) return;
    if (s + 2 * (n - v) / R[u + 1] >= res) return;

    if(!u)
    {
        if(v == n) res = s;
        return;
    }

    //搜索顺序,先R后H,从大到小
    for(int r = min(R[u + 1] - 1,(int)sqrt((n - v - minv[u - 1]) / u)); r >= u; r--)
        for(int h = min(H[u + 1] - 1, (n - v - minv[u - 1]) / r / r); h >= u; h--)
        {
            H[u] = h, R[u] = r;

            //最底层的时候需要加上r*r
            int t = u == m ? r * r : 0;

            dfs(u - 1, v + r * r * h, s + 2 * r * h + t);
        }
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        minv[i] = minv[i - 1] + i * i * i;
        mins[i] = mins[i - 1] + 2 * i * i;
    }

    //m+1层不存在,作为哨兵,减少边界情况的判断
    R[m + 1] = H[m + 1] = INF;

    dfs(m, 0, 0);

    if(res == INF) res = 0;
    cout << res << endl;


    return 0;
}

作者:random_srand
链接:https://www.acwing.com/solution/content/31876/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的:(AcWing算法提高,算法,广度优先,深度优先,迭代加深,剪枝)