DFS(四):剪枝策略
顾名思义,剪枝就是通过一些判断,剪掉搜索树上不必要的子树。在采用DFS算法搜索时,有时候我们会发现某个结点对应的子树的状态都不是我们要的结果,这时候我们没必要对这个分支进行搜索,砍掉这个子树,就是剪枝。
在DFS搜索算法中,剪枝策略就是寻找过滤条件,提前减少不必要的搜索路径。应用剪枝策略的核心问题是设计剪枝判断方法,即确定哪些枝条应当舍弃,哪些枝条应当保留的方法。
剪枝策略按照其判断思路可大致分成两类:可行性剪枝及最优性剪枝。
1.可行性剪枝
可行性剪枝就是把能够想到的不可能出现的情况给它剪掉 。该方法判断继续搜索能否得出答案,如果不能直接回溯。
【例1】Sum It Up (POJ 1564)
Description
Given a specified total t and a list of n integers, find all distinct sums using numbers from the list that add up to t. For example, if t = 4, n = 6, and the list is [4, 3, 2, 2, 1, 1], then there are four different sums that equal 4: 4, 3+1, 2+2, and 2+1+1. (A number can be used within a sum as many times as it appears in the list, and a single number counts as a sum.) Your job is to solve this problem in general.
Input
The input will contain one or more test cases, one per line. Each test case contains t, the total, followed by n, the number of integers in the list, followed by n integers x 1 , . . . , x n . If n = 0 it signals the end of the input; otherwise, t will be a positive integer less than 1000, n will be an integer between 1 and 12 (inclusive), and x 1 , . . . , x n will be positive integers less than 100. All numbers will be separated by exactly one space. The numbers in each list appear in nonincreasing order, and there may be repetitions.
Output
For each test case, first output a line containing `Sums of', the total, and a colon. Then output each sum, one per line; if there are no sums, output the line `NONE'. The numbers within each sum must appear in nonincreasing order. A number may be repeated in the sum as many times as it was repeated in the original list. The sums themselves must be sorted in decreasing order based on the numbers appearing in the sum. In other words, the sums must be sorted by their first number; sums with the same first number must be sorted by their second number; sums with the same first two numbers must be sorted by their third number; and so on. Within each test case, all sums must be distinct; the same sum cannot appear twice.
Sample Input
4 6 4 3 2 2 1 1
5 3 2 1 1
400 12 50 50 50 50 50 50 25 25 25 25 25 25
0 0
Sample Output
Sums of 4:
4
3+1
2+2
2+1+1
Sums of 5:
NONE
Sums of 400:
50+50+50+50+50+50+25+25+25+25
50+50+50+50+50+25+25+25+25+25+25
(1)编程思路。
由于题中给出待选数的顺序就是从大到小的,因此我们从第一个数开始,依次往后搜索,将可能的数据都记录下来,每遇到一种满足题意的组合就输出,一直搜索下去,得到所有答案。若没有答案,输出NONE。
定义全局数组int a[12]来保存给出的待选数列表,为输出和式,定义int b[12]保存选中的数据。
递归函数void dfs(int i,int j,int sum)表示从数组a的第i个元素开始选择数据加入到和值sum中,选中的数据a[i]保存到b[j]中。
因为题目中所有待选数都是正数,因此一旦发现当前的和值sum都已经大于t了,那么之后不管怎么选,和值都不可能回到t,所有当sum > t时,可以直接剪掉。
if (sum > t) return;
由于给出的N个数中可以有重复的数,求N个数中取若干个数,这若干个数的和为T的所有情况,但这些情况不能重复。因此,程序中还需要去重。
如果不去重,则对于第1组测试数据4 6 4 3 2 2 1 1,会输出
Sums of 4:
4
3+1 // 式子中的1是倒数第2个1
3+1 // 式子中的1是最后1个1
2+2
2+1+1 // 式子中的2是第3个2
2+1+1 // 式子中的2是第4个2
由于待选数从大到小排列,相同的数据连续放在一起,因此用循环
while(k<n && a[k]==a[k+1]) k++;
可以简单地去重。即某个数作为和式中的加数第1次被选中时,其后连续相同的数不能作为和式中的第1次被选中的加数。
这个去重操作也会剪掉一些枝叶,也可以看成是一个剪枝。
(2)源程序。
#include <stdio.h>
#define N 12
int a[N],b[N];
int t,n,ok;
void dfs(int i,int j,int sum)
{
int k;
if(sum>t) return; // 剪枝1
if(sum==t)
{
printf ("%d",b[0]);
for (k=1;k<j;k++)
printf("+%d",b[k]);
printf("\n");
ok=1;
return;
}
for(k=i;k<n;k++)
{
b[j]=a[k];
dfs(k+1,j+1,sum+a[k]);
while(k<n && a[k]==a[k+1]) // 去重
k++;
}
}
int main()
{
int sum;
while (1)
{
scanf("%d%d",&t,&n);
if (t==0 && n==0) break;
sum=0;
for(int i=0;i<n;i++)
{
scanf("%d",&a[i]);
sum=sum+a[i];
}
printf("Sums of %d:\n",t);
ok=0;
if (sum<t)
{
printf("NONE\n");
continue;
}
else
dfs(0,0,0);
if(!ok)
printf("NONE\n");
}
return 0;
}
【例2】Sticks (POJ 1011)。
Description
George took sticks of the same length and cut them randomly until all parts became at most 50 units long. Now he wants to return sticks to the original state, but he forgot how many sticks he had originally and how long they were originally. Please help him and design a program which computes the smallest possible original length of those sticks. All lengths expressed in units are integers greater than zero.
Input
The input contains blocks of 2 lines. The first line contains the number of sticks parts after cutting, there are at most 64 sticks. The second line contains the lengths of those parts separated by the space. The last line of the file contains zero.
Output
The output should contains the smallest possible length of original sticks, one per line.
Sample Input
9
5 2 1 5 2 1 5 2 1
4
1 2 3 4
0
Sample Output
6
5
(1)编程思路。
定义数组int* stick=new int[n];来保存所输入的n根木棒的长度,数组bool* visit=new bool[n]; 来记录对应的n根木棒是否被用过,初始时值全为false。
由于木棒越长,拼接灵活度越低,因此搜索前先对所有的棒子按降序排序,即将stick数组的元素按从大到小排序。
编写一个递归的搜索函数bool dfs(int* stick,bool* visit,int len,int InitLen,int s,int num),其中参数 len表示当前正在组合的棒长、InitLen表示所求的目标棒长、s(stick[s])表示搜索的起点、num代表已用的棒子数量。如果按InitLen长度拼接,可以将n根木棒全用掉,则函数返回true,搜索成功;否则函数返回false,表示InitLen不可能是所求的最短原始棒长。
令InitLen为所求的最短原始棒长,maxlen为给定的棒子堆中最长的棒子(maxlen=stick[0]);,sumlen为这堆棒子的长度之和,那么所要搜索的原始棒长InitLen必定在范围[maxlen,sumlen]中。
实际上,如果能在[maxlen,sumlen-InitLen]范围内找到最短的InitLen,该InitLen必也是[maxlen,sumlen]范围内的最短;若不能在[maxlen,sumlen-InitLen]范围内找到最短的InitLen,则必有InitLen=sumlen。因此,可以只在[maxlen,sumlen-InitLen]范围内搜索原始棒长。即搜索的循环为:for(int InitLen=maxlen;InitLen<=sumlen-InitLen;InitLen++)
在搜索时,为提高效率,还可以进行剪枝。具体是:
1)由于所有原始棒子等长,那么必有sumlen%Initlen==0,因此,若sumlen%Initlen!=0,则无需对Initlen值进行搜索判断。
2)由于所有棒子已降序排列,在DFS搜索时,若某根棒子不合适,则跳过其后面所有与它等长的棒子。
3)对于某个目标InitLen,在每次构建新的长度为InitLen的原始棒时,检查新棒的第一根棒子stick[i],若在搜索完所有stick[]后都无法组合,则说明stick[i]无法在当前组合方式下组合,不用往下搜索(往下搜索只会令stick[i]被舍弃),直接返回上一层。
(2)源程序
#include<iostream>
#include<algorithm>
using namespace std;
int n; // 木棒数量
int cmp(const void* a,const void* b)
{
return *(int*)b-*(int*)a;
}
bool dfs(int* stick,bool* visit,int len,int InitLen,int s,int num);
int main()
{
while(cin>>n && n)
{
int* stick=new int[n];
bool* visit=new bool[n];
int sumlen=0,i;
bool flag;
for(i=0;i<n;i++)
{
cin>>stick[i];
sumlen+=stick[i];
visit[i]=false;
}
qsort(stick,n,sizeof(stick),cmp);
int maxlen=stick[0];
flag=false;
for(int InitLen=maxlen;InitLen<=sumlen-InitLen;InitLen++)
{
if(!(sumlen%InitLen) ) // 剪枝(1)
if (dfs(stick,visit,0,InitLen,0,0))
{
cout<<InitLen<<endl;
flag=true;
break;
}
}
if(!flag)
cout<<sumlen<<endl;
delete stick;
delete visit;
}
return 0;
}
bool dfs(int* stick,bool* visit,int len,int InitLen,int s,int num)
{
if(num==n)
return true;
int sample=-1;
for(int i=s;i<n;i++)
{
if(visit[i] || stick[i]==sample)
continue; // 剪枝(2)
visit[i]=true;
if(len+stick[i]<InitLen)
{
if(dfs(stick,visit,len+stick[i],InitLen,i,num+1))
return true;
else
sample=stick[i];
}
else if(len+stick[i]==InitLen)
{
if(dfs(stick,visit,0,InitLen,0,num+1))
return true;
else
sample=stick[i];
}
visit[i]=false;
if(len==0) // 剪枝(3)
break;
}
return false;
}
2.最优性剪枝
最优性剪枝,又称为上下界剪枝,是一种重要的剪枝策略。它记录当前得到的最优值,如果当前结点已经无法产生比当前最优解更优的解时,可以提前回溯。
对于求最优解的一类问题,通常可以用最优性剪枝。比如在求迷宫最短路的时候,如果发现当前的步数已经超过了当前最优解,那从当前状态开始的搜索都是多余的,因为这样搜索下去永远都搜不到更优的解。通过这样的剪枝,可以省去大量冗余的计算。
John and Brus are inside the vault at the moment. They would like to steal everything, but unfortunately they are able to carry diamonds with the total weight not exceeding M.
Your task is to help John and Brus to choose diamonds with the total weight less than or
equal to M and the maximal possible total cost.
Input
Output
Sample Input
2 2 4 3 2 5 3 3 100 4 7 1 5 9 2
Sample Output
6 29
Hint
1 ≤ T ≤ 74,
1 ≤ N ≤ 15,
1 ≤ M ≤ 1000000000 (109),
1 ≤ Wk, Ck ≤ 1000000000 (109).
(1)编程思路。
利用贪心的思想先对箱子进行排序,关键字为性价比(Ck/Wk)。也就是单位重量的价值最高的排第一。搜索的时候采用的剪枝策略有:
剪枝1:之后所有的钻石价值+目前已经得到的价值<=ans,则剪枝。
剪枝2:剩下的重量全部装目前最高性价比的钻石+目前已经得到的价值<=ans,则剪枝。
(2)源程序。
#include <iostream>
#include<algorithm>
using namespace std;
typedef __int64 ll;
int m,n;
ll ans;
bool flag;
struct node
{
ll w,c;
int num;
}lcm[20];
ll bsum[20];
int cmp(struct node a,struct node b)
{
return a.c * b.w > b.c * a.w; // 按性价比降序排列
}
void dfs(int cur,ll sum,int remain)
// cur搜到的当前位置,sum当前的总价值,remain当前还剩多少重量
{
if(remain < 0) return;
if(flag) return;
if(cur == n)
{
if(ans < sum)
ans = sum;
return;
}
if(sum + bsum[cur] <= ans) return; // 剪枝1
if(sum + remain*(lcm[cur].c*1.0/lcm[cur].w) <= ans) // 剪枝2
return;
if(remain == 0) // 因为先贪心了一下,所以第一次恰好凑成m的重量的一定是最优解
{
ans = sum;
flag = true;
return;
}
for(int i = lcm[cur].num;i >= 0;i --)
{
if (remain >= i * lcm[cur].w)
dfs(cur + 1,sum + i * lcm[cur].c,remain - i * lcm[cur].w);
else
ans = sum > ans?sum:ans;
}
}
int main()
{
int t,i;
scanf("%d",&t);
while(t--)
{
ll sumw,sumc;
sumw = sumc = 0;
scanf("%d%d",&n,&m);
for(i = 0;i < n;i ++)
{
scanf("%I64d",&lcm[i].w);
lcm[i].num = i + 1;
sumw += lcm[i].w * (i + 1);
}
for(i = 0;i < n;i ++)
{
scanf("%I64d",&lcm[i].c);
sumc += lcm[i].c * (i + 1);
}
if(sumw <= m)
{
printf("%I64d\n",sumc);
continue;
}
sort(lcm,lcm + n,cmp);
memset(bsum,0,sizeof(bsum));
for(i = n - 1;i >= 0;i --)
{
bsum[i] = lcm[i].num * lcm[i].c;
bsum[i] += bsum[i + 1];
}
flag = false;
ans = 0;
dfs(0,0,m);
printf("%I64d\n",ans);
}
return 0;
}