目录
DFS剪枝与优化
0)剪枝优化策略
1)小猫爬山
2)数独
3)木棒
4)生日蛋糕(数学剪枝!!!)
DFS剪枝与优化 0)剪枝优化策略
来自yxc算法提高课 的大佬:深度优先搜索(DFS)的剪枝方式 - AcWing
1)小猫爬山
输入概要
第一行为 小猫的总数n 缆车的最大承受重量
接下来的n行为 :各个小猫的重量
核心思路:
- 暴力搜索dfs
- 对于每一个小猫,有两种选择,让它上这辆缆车,或这不让它上这辆缆车
- 如果枚举到了最后一支小猫,那么此时就记录下缆车数量的最小值 即可
剪枝优化:
- 优化搜索顺序:将小猫按重量从大到小排序进行搜索,这样可以减少分支
- 可行性剪枝:判断小猫上这辆缆车与否,关键是要看 缆车上已有小猫的重量+当前枚举小猫的数量是否大于给定的限制的重量,如果是小于等于给定的限制重量,那么就可以将该小猫上车,如果是大于给定的限制的重量,那么就将这个小猫安置在新开的缆车上
- 最优化剪枝:如果当前枚举的缆车数量 已经 大于等于 之前得到的“最小”缆车数量 那么就可以提前剪枝并返回,避免没有必要的搜索
完整代码:
#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)数独
核心思路:
- 爆搜
,但这个好像是废话 - 对于每一个格子,依次枚举其合理数字的情况 ,重复此 *** 作
- 若对于(2) *** 作时 ,有冲突错误,证明 某一步发生错误 进行回溯判断,因此证明了我们的爆搜函数 是判断函数bool
- 那么当格子被填满且没有发生错误,那么此时就是一种正确情况,此时就正解
剪枝优化::
毫无疑问:上述的这个步骤会超时,因此需要剪枝优化
- 优化剪枝顺序:对于一个‘.’ 就是我们要填入的数字,那么我们希望它的数字的可能种类较少,这样我们枚举的分支才会减少,所以先从数字的可能种类少的开始枚举
- 预处理优化判断:(1)中说到,要判断一个 ' . ' 的数字的可能种类,那么数独的规则就是每一行,每一列,每一个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~2^9次方 中 数字对应 数字'1'出现的个数
- 然后写一个判断函数 ,判断行 + 列 + 宫格 中 1 的个数
- 因为要枚举1的个数,所有要快速 知道 二进制中的 '1' 出现在 哪 一个位置上,这里使用算法基础课的lowbit 函数,O(1)时间内求出 pow(2,最后一位1的位置)的数值大小
- 因为这个(3)所以,我们要预处理 log2 M 对应的数,这样才能得到我们想要得到的位置对对应的数字 ,用map数组进行存储
- 初始化:因为这道题有多组输入,所以一开始在没有输入数字之前,所有的点 都可以选择 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)木棒
样例解释:
第一行输入: 小木棍的个数
第二行输入:小木棍的长度
输出:木棒的最小长度(注意是等长的木棒)
对于第一组样例:
(5+1) (5+1) (5+1) (2+2+2) 木棒的最小长度为 6
对于第二组样例:
(1+4) (2+3) 木棒的最小长度为:5
核心思路:
大木棒的总长度==等长的木棒拼接而成
参考自:AcWing 167. 木棒(详细证明,图解版) - AcWing
- 找到不变的量:砍之前 大木棒的总长度==木棒*木棒的数量,对于这个等式而言,要使得木棒的长度最小,大木棒的总长度又不能改变,那么只需增加木棒的数量,并记录木棒的长度增加
- 剩下的就是小猫爬山一样的整体思路,将木棍进行分组,如果组已有的长度+当前遍历到的木棍的长度>限定的长度,那么就将该木棍加到一个新的分组中
剪枝优化:
- 剪枝顺序优化:这个问题中木棍的长度跟搭配的顺序没有关系,这个是
显然的 - 剪枝顺序优化:从木棍长度长的一端开始枚举,这样会使后序的分支减少
- 剪枝等效冗余:如果第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)生日蛋糕(数学剪枝!!!)
#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
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)