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.最優性剪枝

      最優性剪枝,又稱為上下界剪枝,是一種重要的剪枝策略。它記錄當前得到的最優值,如果當前結點已經無法產生比當前最優解更優的解時,可以提前回溯。
      對於求最優解的一類問題,通常可以用最優性剪枝。比如在求迷宮最短路的時候,如果發現當前的步數已經超過了當前最優解,那從當前狀態開始的搜索都是多余的,因為這樣搜索下去永遠都搜不到更優的解。通過這樣的剪枝,可以省去大量冗余的計算。

【例3】The Robbery (POJ 3900)。
Description
In the downtown of Bucharest there is a very big bank with a very big vault. Inside the vault there are N very big boxes numbered from 1 to N. Inside the box with number k there are k very big diamonds, each of weight Wk and cost Ck. 
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

The first line contains single integer T – the number of test cases. Each test case starts with a line containing two integers N and M separated by a single space. The next line contains N integers Wk separated by single spaces. The following line contains N integers Ck separated by single spaces.

Output

For each test case print a single line containing the maximal possible total cost of diamonds.

Sample Input

2 
2 4 
3 2 
5 3 
3 100 
4 7 1 
5 9 2

Sample Output

6 
29

Hint

Constraints: 
1 ≤ T ≤ 74, 
1 ≤ N ≤ 15, 
1 ≤ M ≤ 1000000000 (10 9), 
1 ≤ Wk, Ck ≤ 1000000000 (10 9).

      (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;

}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM