1. 程式人生 > >搜尋(3):重複性剪枝 (poj1011)

搜尋(3):重複性剪枝 (poj1011)

POJ 1011

在民國某年,少林寺被軍閥炮轟,這些棍子被炸成 N 節長度各異的小木棒

戰火過後,少林方丈想要用這些木棒拼回原來的棍子

可他記不得原來到底有幾根棍子了,只知道古人比較矮,且為了攜帶方便,棍子一定比較短

他想知道這些棍子最短可能有多短

分析

·
·

嘗試 (列舉) 什麼?

列舉所有可能的棍子長度

從最長的那根木棒的長度一直列舉到木棒長度總和的一半

對每個假設的棍子長度,試試看能否拼齊若干根棍子

·
·

真的要每個長度都試嗎?

對於不是木棒總長度的因子的長度,可以直接否定,不需嘗試

·
·

假設了一個棍子長度的前提下,如何嘗試去拼成若干根該長度的棍子?

一根一根地拼棍子

如果拼好前i根棍子,結果發現第i+1根無論如何拼不成了 
    →推翻第i根的拼法,重拼第i根…..

直至有可能推翻第1根棍子的拼法

·
·

本題真正應該設定的狀態是什麼

狀態可以是一個二元組 (R, M)

R : 還沒被用掉的木棒數目
M : 當前正在拼的棍子還缺少的長度

初始狀態和搜尋的終止狀態(解狀態)是什麼?

假設共有N節木棒,假定的棍子長度是L:

初始狀態: (N, L)
終止狀態: (0, 0)

·
·
·

剪枝方案

·
·

不要在同一個位置多次嘗試相同長度的木棒、

如果某次拼接選擇長度為S 的木棒,導致最終失敗,則在同一位置嘗試下一根木棒時,要跳過所有長度為S 的木棒

·
·

不考慮替換第i根棍子中的第一根木棒(換了也沒用)

可以考慮把木棒2, 3換掉重拼棍子i,但是把2, 3都去掉後,換1是沒有意義的

因為假設替換後能全部拼成功,那麼這被換下來的第一根木棒,必然會出現在以後拼好的某根棍子k中

那麼我們原先拼第i根棍子時, 就可以用和棍子k同樣的構成法來拼,照這種構成法拼好第i根棍子,繼續下去最終也應該能夠全部拼成功

這就是一種去重複性的搜尋
·
·

不要希望通過僅僅替換已拼好棍子的最後一根木棒就能夠改變失敗的局面

假設替換3後最終能夠成功,那麼3必然出現在後面的某個棍子k裡

將棍子k中的3和棍子i中用來替換3的幾根木棒對調,結果當然一樣是成功的

這就和i原來的拼法會導致不成功矛盾
·
·

確保長度是從長到短排列

木棒3 比木棒2長,這種情況的出現是一種浪費

因為要是這樣往下能成功,那麼2, 3 對調的拼法肯定也能成功。

由於取木棒是從長到短的,所以能走到這一步,就意味著當初將3放在2的位置時,是不成功的

具體方法:

為此,要設定一個全域性變數 nLastStickNo,記住最近拼上去的那條木棒的下標。
·
·

程式碼

#include <iostream>
#include <memory.h>
#include <stdlib.h>
#include <vector>
#include <algorithm>
using namespace std;
int N;
int L;
vector<int> anLength;
int anUsed[65]; //是否用過的標記
int i, j, k;
int nLastStickNo;

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

int Dfs(int R, int M)
{
    if (R == 0 && M == 0)
        return true;
    if (M == 0) //一根剛剛拼完
        M = L; //開始拼新的一根
    int nStartNo = 0;
    if (M != L) //剪枝4
        nStartNo = nLastStickNo + 1;
    for (int i = nStartNo; i < N; i++) 
    {
        if (!anUsed[i] && anLength[i] <= M) 
        {
            if (i > 0)
            {
                if (anUsed[i - 1] == false
                    && anLength[i] == anLength[i - 1])
                    continue; //剪枝1
            }
            anUsed[i] = 1; nLastStickNo = i;
            if (Dfs(R - 1,M - anLength[i]))
                return true;
            else {
                anUsed[i] = 0; //說明本次不能用第i根
                               //第i根以後還有用
                if (anLength[i] == M || M == L)
                    return false; //剪枝3, 2
            }
        }
    }
    return false;
}

int main()
{
    while (1) {
        cin >> N;
        if (N == 0)
            break;
        int nTotalLen = 0;
        anLength.clear();
        for (int i = 0; i < N; i++) {
            int n;
            cin >> n;
            anLength.push_back(n);
            nTotalLen += anLength[i];
        }
        sort(anLength.begin(), anLength.end(),cmp); //要從長到短進行嘗試
        for (L = anLength[0]; L <= nTotalLen / 2; L++) {
            if (nTotalLen % L)
                continue;
            memset(anUsed, 0, sizeof(anUsed));
            if (Dfs(N, L)) {
                cout << L << endl;
                break;
            }
        }
        if (L > nTotalLen / 2)
            cout << nTotalLen << endl;
    } // while
    return 0;
}

剪枝總結

1)選擇特定的搜尋順序

如果一個任務分為 A, B, C…..等步驟(先後次序無關)

要優先嚐試可能性少的步驟

這樣可以儘早的排除不可能的情況, 減少搜尋量

·
2)要發現表面上不同,實質相同的重複狀態

避免重複的搜尋, 就上上文對於第一根和第二根棍子的更換

·
3)要根據實際問題發掘剪枝方案

廢話。。。