DFS
递归
举个例子,将
暴力一点的方式是写
我们可以先枚举第一个数字是多少,比如是
变成了一个类似的子问题,可以考虑用递归来实现:
int a[1001];//a数组负责存答案
int k;//一共要分解成k个数字
void dfs(int n,int k)//将n分解成k个数字之和
{
if(k==0)//数字用完了
{
if(n==0)//分解完成
{
for(int i=1;i<=now;++i) cout<<a[i]<<' ';
cout<<'\n';
}
return;
}
for(int i=1;i<=n;++i)//这个位置用的数字是多少
{
a[now]=i;
dfs(n-i,k-1);//下一个位置,要分解的数字减小了i,个数减少了一个。
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
回溯
加强一下刚才的例子:
将
除了枚举数字之余,还需要记录一下现在有哪些数字还没用到。
因为这个信息量比较大,所以用全局变量而不是局部变量来记录。
int a[1001];//a数组负责存答案
int k;//一共要分解成k个数字
bool vis[1001];// vis[i] 表示这个数字有没有被用过
void dfs(int n,int k)//将n分解成k个数字之和
{
if(k==0)//数字用完了
{
if(n==0)//分解完成
{
for(int i=1;i<=now;++i) cout<<a[i]<<' ';
cout<<'\n';
}
return;
}
for(int i=1;i<=n;++i) if(!vis[i])//这个数字还没被用过
{
vis[i]=1;
a[now]=i;
dfs(n-i,k-1);//下一个位置,要分解的数字减小了i,个数减少了一个。
a[now]=0;
vis[i]=0;//因为是全局变量,所以当我们选择下一个数字的时候,要把上一次选择的影响消除。
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
减枝
(其实这个定义并不严格,因为有些状态会到达同一个节点,比如
所以,我们优化
记忆化
上面的例子中,前两个数选
如果我们只计算方案数而不关心每个方案具体长什么样,那么就可以把
因为有可能本来就是
int k;//一共要分解成k个数字
int dp[1010][1010];//记得全部初始化成 -1
bool vis[1001];
int dfs(int n,int k)//将n分解成k个数字之和
{
if(k==0)//已经有k个数字了
{
if(n==0) return 1;//分解完成,多了一种方案
return 0;
}
if(dp[n][k]!=-1) return dp[n][k];
int ans=0;
for(int i=1;i<=n;++i) if(!vis[i])
{
vis[i]=1;
ans+=dfs(n-i,k-1);//下一个位置,要分解的数字减小了i,个数减少了一个。
vis[i]=0;
}
dp[n][k]=ans;
return dp[n][k];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
可行性剪枝
还是这个例子,在枚举这个位置用的数字是多少的时候,其实不用枚举到
现在还需要分解成
for(int i=1;i<=n-k+1;++i) if(!vis[i])
{
vis[i]=1;
ans+=dfs(n-i,k-1);//下一个位置,要分解的数字减小了i,个数减少了一个。
vis[i]=0;
}
2
3
4
5
6
最优性剪枝
有的搜索题是要求答案的最小值,那么我们用一个全局变量记录现在已经找到的答案的最小值是多少。
如果现在状态的答案比已经搜到的最小值还大,就直接返回。
int ans=1e9;
void dfs(int now,int ret)//now是状态参数,ret是这个状态的答案
{
if(ret>ans) return;//现在的答案比已经搜到的答案还要差,没必要再搜下去了,当然前提是这个ret不会再变小了!!!
/*
继续搜索
*/
}
2
3
4
5
6
7
8
例题
7 月 17 日是 Mr.W 的生日,ACM-THU 为此要制作一个体积为
设从下往上数第
由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面(最下一层的下底面除外)的面积
请编程对给出的
(除
样例输入输出
100
2
2
68
表面积可以表示成最底下一圈的原的面积加上所有层的侧面积,可以先枚举最底下是多大,然后在途中只计算侧面积。
可以直接考虑 dfs(now,v,ret,prer,preh)
表示还剩
int ans=1e9;
void dfs(int now,int v,int ret,int prer,preh)
{
if(now==0)
{
if(v==n) ans=min(ans,ret);
return;
}
for(int i=1;i<prer;++i)//要比上一层半径小,所以到pre-1为止。
{
for(int j=1;j<preh;++j)
{
dfs(now-1,v+j*i*i,ret+2*i*j,i,j);
}
}
}
for(int i=1;i<=n;++i)
{
for(int j=1;j<=n;++j)//最地下一层的高度和半径直接枚举。
{
dfs(m-1,i*i*j,i*i+2*i*j,i,j);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这样肯定会超时啦,想想怎么优化吧~
(一)可行性剪枝:
剩下
剩下
假设先枚举半径,再枚举高度,因为要保证后面的层数还能放下蛋糕,所以当半径是
(二)最优性剪枝:
如果还剩
所以剩余的侧面积至少是
(三)改变枚举顺序:
如果我们从小到大去枚举半径和高度,那么对体积的削弱较小,后续可选择的方案更多。
如果从大到小去枚举,那么后续可选择的方案更少,节点状态数就会变少。
同时更容易早一步计算出一个当前的答案,最大化利用最优性剪枝。
int ans=1e9;
int n,m;
int b[21];
void dfs(int now,int v,int ret,int prer,int preh)
{
if(now==0)
{
if(v==n) ans=min(ans,ret);
return;
}
if(v+b[now]>n) return;//如果现在的体积加上剩下now层最少需要的体积大于n则返回
if(ret+2*(n-v)/prer>=ans) return;//如果现在的表面积加上剩余now层最少的表面积大于等于ans则返回
//从大到小枚举,减少搜索树枝条
for(int i=prer-1;i>=now;--i)//要比上一层半径小,所以到pre-1为止。
{
int h=min(preh-1,(n-v-b[now-1])/(i*i));//选择半径i的时候高度j的最大值
for(int j=h;j>=now;--j)
{
dfs(now-1,v+j*i*i,ret+2*i*j,i,j);
}
}
}
void solve()
{
cin>>n>>m;
for(int i=1;i<=20;++i)
{
b[i]=b[i-1]+i*i*i;//b[i]是剩下i层最少需要多少体积
}
for(int i=n;i>=m;--i)
{
int h=(n-b[m-1])/(i*i);
for(int j=h;j>=m;--j)
{
dfs(m-1,i*i*j,i*i+2*i*j,i,j);
}
}
cout<<ans<<'\n';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
meet in middle
给定
显然每个数字有选或者不选两种情况,所以直接搜索的时间复杂度是
将数字分成两组,前
每组的时间复杂度都是
将两组数字分别从小到大排序。
枚举其中一组数字,假如当前枚举到的数字是
求另一组中有多少大于
时间复杂度变成了
练习题
P1219 [USACO1.5] 八皇后 Checker Challenge
P3067 [USACO12OPEN] Balanced Cow Subsets G
P1120 小木棍 (选做,剪枝程度非常变态)