NPC問題及其解決方法(回溯法、動態規划、貪心法、深度優先遍歷)


NP問題(Non-deterministic Polynomial )多項式復雜程度的非確定性問題,這些問題無法根據公式直接地計算出來。比如,找大質數的問題(有沒有一個公式,你一套公式,就可以一步步推算出來,下一個質數應該是多少呢?這樣的公式是沒有的);再比如,大的合數分解質因數的問題(有沒有一個公式,把合數代進去,就直接可以算出,它的因子各自是多少?也沒有這樣的公式)。

NPC問題(Non-deterministic Polynomial complete):NP完全問題,可以這么認為,這種問題只有把解域里面的所有可能都窮舉了之后才能得出答案,這樣的問題是NP里面最難,但是這樣算法的復雜程度,是指數關系。一般說來,如果要證明一個問題是NPC問題的話,可以拿已經是NPC問題的一個問題經過多項式時間的變化變成所需要證明的問題,那么所要證明的問題就是一個NPC問題了。NPC問題是一個問題族,如果里面任意一個問題有了多項式的解,即找到一個算法,那么所有的問題都可以有多項式的解。

著名的NPC問題:

背包問題(Knapsack problem):01背包是在M件物品取出若干件放在空間為W的背包里,每件物品的體積為W1,W2……Wn,與之相對應的價值為V1,V2……Vn。求出獲得最大價值的方案。

旅行商問題(Traveling Saleman Problem,TSP),該問題是在尋求單一旅行者由起點出發,通過所有給定的需求點之后,最后再回到原點的最小路徑成本。

哈密頓路徑問題(Hamiltonian path problem)與哈密頓環路問題(Hamiltonian cycle problem)為旅行推銷員問題的特殊案例。哈密頓圖:由指定的起點前往指定的終點,途中經過所有其他節點且只經過一次。

歐拉回路(從圖的某一個頂點出發,圖中每條邊走且僅走一次,最后回到出發點;如果這樣的回路存在,則稱之為歐拉回路。)與歐拉路徑(從圖的某一個頂點出發,圖中每條邊走且僅走一次,最后到達某一個點;如果這樣的路徑存在,則稱之為歐拉路徑。)

  • 無向圖歐拉回路存在條件:所有頂點的度數均為偶數。
  • 無向圖歐拉路徑存在條件:至多有兩個頂點的度數為奇數,其他頂點的度數均為偶數。
  • 有向圖歐拉回路存在條件:所有頂點的入度和出度相等。
  • 有向圖歐拉路徑存在條件:至多有兩個頂點的入度和出度絕對值差1(若有兩個這樣的頂點,則必須其中一個出度大於入度,另一個入度大於出度),其他頂點的入度與出度相等。

 

01背包問題解決方法

法I:回溯法遞歸

public:
void Knapsack(int *w,int *v, int c,int n){//w:容量;v:value
    this->c = c;
    this->n = n;
    bestv = 0;
    bool x[n] = {false}; //x: 是否選擇這個物品
    backtracking(w,v,x,0);
}

void backtracking(int depth, int *w, int *v, bool *x){ 
    if(depth >= n){
        if(tmpV > bestv){
            bestv = tmpV;
            for(int i = 0; i < n; i++){
                bestx[i] = x[i];
            }
        }
        return;
    }
    if(tmpW + w[depth] <= c){ //加入當前元素
        x[i] = true;
        tmpW += w[depth];
        tmpV += v[depth];
        backtracking(depth+1, w, v, x);
        tmpV -= v[depth]; //backtrack
        tmpW -= w[depth];
        x[i] = false;
    }

    backtracking(depth+1, w, v, x);//不加入當前元素
}

private:
    int bestv; //最優方法的價值
    int* bestx; //最優方法選取的物品
    int tmpV; //已有價值
    int tmpW; //已使用的容量
    int c; //背包容量
    int n; //物品數量

 

法II:動態規划

1。定義階段:v[i-1]表示第i個物品的價值
2。定義狀態:V[n+1][C]前i個物品裝入容量為j的背包中獲得的最大價值
3。狀態轉移方程:V[i][j]=max(V[i-1][j],V[i-1][j-w[i-1]]+v[i-1]);
4。定義邊界條件:V[i][0]=0;V[0][j]=0;

int KnapSack(int n,int w[],int v[],int x[],int C){
    int V[n+1][C];//前i個物品裝入容量為j的背包中獲得的最大價值
    int i,j;
    for(i=0;i<=n;i++)
        V[i][0]=0;
    for(j=0;j<=C;j++)
        V[0][j]=0;
    for(i=1;i<=n-1;i++)
        for(j=1;j<=C;j++)
            if(j < w[j]) V[i][j]=V[i-1][j];
            else  V[i][j]=max(V[i-1][j],V[i-1][j-w[i-1]]+v[i-1]);

     //標示哪些物品被放入
     j=C;
     for(i=n;i>0;i--)
         if(V[i][j]>V[i-1][j]){
             x[i-1]=1;
             j=j-w[i-1];
         }
         else x[i-1]=0;
     return V[n][C];
}

 

法III: 貪心法解決普通背包問題

普通背包問題:與0-1背包問題類似,所不同的是在選擇物品i裝入背包時,可以選擇物品i的一部分,而不一定要全部裝入背包,1≤i≤n。

貪心准則:每一項計算yi=vi/wi,再按比值的降序來排序,從第一項開始裝背包,然后是第二項,依次類推,盡可能的多放,直到裝滿背包。適用於普通背包問題,但不適用於01背包問題。

 

旅行商問題解決方法

法I:回溯法遞歸

int bestd;
vector< int > bestv;//保存最優解路徑上的節點
int tmpSum;
vector< int > tmpV; //暫時保存路徑上的節點
unordered_set visited; 

void shortest( int **d,int n){
    bestd = INT_MAX;
    dfs(d,n,0);
}

void dfs (int **d,int n, int depth){
    tmpV.push_back(depth);
    if(tmpV.size()==n){
        tmpSum += d[depth][0];

        if(tmpSum < bestd){
            bestd = tmpSum;
            bestv = tmpV;
        }
        tmpSum -= d[depth][0]; //backtrack
        tmpV.pop();
        return;
    }
    visited.insert(depth);
    for(int i = 0; i < n; i ++){
        if(visited.find(i) != visited.end() && tmpSum + d[depth][i] < bestd){
            tmpSum += d[depth][i];
            dfs(d,n,i);
            tmpSum -= d[depth][i]; //backtrack
       }
    }
    visited.erase(depth); //backtrack
    tmpV.pop_back();
}

 

法II:動態規划

1。定義階段:v[i-1]表示第i個物品的價值
2。定義狀態:F[i][j]表示當前從i結點出發已訪問j中節點的情況下的最短距離。其中,i表示當前訪問的節點,i∈[0,n-1];j=已訪問的節點的bitmap,j∈[0,2^(n-1)-1]
3。狀態轉移方程:F[i][j] = min{ min,D[i][k] + F[k][j-(int)pow(2,k-1)]) }
4。定義邊界條件:
F[i][0] = D[i][0]即表示節點i到第一個節點的距離,D是原圖的鄰接矩陣

 void tsp(int** D, int n){
    int i,j,k,min,temp;
    int b=(int)pow(2,n-1); //已遍歷的節點bitmap(除了最后一個節點,每個節點有選擇及不選擇兩種情況)

    //申請二維數組F和M
    int ** F = new int* [n];//n行b列的二維數組,存放階段最優值
    int ** M = new int* [n];//n行b列的二維數組,存放最優策略
    for(i=0;i < n; i++){
        F[i] = new int[b];
        M[i] = new int[b];
    }

    //初始化F[][]和M[][]
    for(i=0;i < n; i++for(j=0;j < n; j++){
            F[j][i] = -1;
            M[j][i] = -1;
        }
    for(i=0;i < n; i++) F[i][0] = D[i][0];

    //狀態轉移
    for(i=1;i < b; i++for(j=1;j < n; j++){
            if( ((int)pow(2,j-1) & i) == 0){//結點j不在i表示的集合中
                min=INT_MAX;
                for(k=1;k < n; k++ ){ //從已訪問過的節點中找出一個到節點j的距離最短
                    if( (int)pow(2,k-1) & i ){//非零表示結點k在集合中
                        temp = D[j][k] + F[k][i-(int)pow(2,k-1)];//去掉k結點即將k對應的二進制位置0
                        if(temp < min){
                            min = temp;
                            F[j][i] = min;//保存階段最優值
                            M[j][i] = k;//保存最優決策
                        }
                    }
            }
        }
    //最后一列,即總最優值的計算
    min=INT_MAX;
    for(k=1;k < n; k++ ){
        //b-1的二進制全1,表示全集
        temp = D[0][k] + F[k][b-1 - (int)pow(2,k-1)]; //去掉k
        if(temp < min){
            min = temp;
            F[0][b-1] = min;
            M[0][b-1] = k;
        }
    }
    cout<<"最短路徑長度:"<<F[0][b-1]<<endl;//最短路徑長度
    cout<<"最短路徑(編號0—n-1):"<<"0"; //最短路徑上的節點
    for(i=b-1,j=0; i>0; ){//i的二進制是5個1,表示集合{1,2,3,4,5}
        j = M[j][i];//下一步去往哪個結點
        i = i - (int)pow(2,j-1);//從i中去掉j結點
        cout<<"->"<<j;
    }
    cout<<"->0"<<endl;
}

 

 法III: 啟發式貪心法

 采用啟發式貪心算法。對於那些受大自然的運行規律或者面向具體問題的經驗、規則啟發出來的方法,人們常常稱之為啟發式算法(Heuristic Algorithm)。啟發式算法得到的解只是近似最優解。步驟:

(1)從旅行商問題的n個城市中選擇1個城市構成部分解序列T1={c1},共有n種初始組合。

(2)從部分解序列之外的城市中選擇一個新的城市k,插到原有的部分解序列Tk-1={c1,c2,…,ck-1}中,得到新的部分解列Tk={c1,c2,…,ck,…,ck-1}。新的城市ck及插入位置由改進的貪心法確定。

用Tk-1={c1, c2,…,ck-1}表示已確定的部分解序列,則由min(d(ci,ck)+d(ck,ci+1) -d(ci,ci+1)),ci,ci+1∈Tk-1,ck∈NP完全問題確定插入的城市ck及插入位置(ci,ck,ci+1)

(3)用冒泡法對新的部分解序列Tk中的每個城市進行可能優化游路的換位、移位和倒位操作,直到不再能通過這些操作優化游路。

對於旅行商問題的一個解序列,可以通過換位、移位和倒位三種基本的次序變換操作,改變原來解序列的排列次序,得到新的解序列。其它游路改進的啟發式操作,都可以由這三種基本操作組合而成。

換 位操作(exchange):將解序列中第i個元素ci與第j個元素cj的位置交換。ΔD換位=(d(ci- 1,ci)+d(ci,ci+1)+d(cj-1,cj)+d(cj,cj+1))-(d(ci-1,cj)+d(cj,ci+1)+d(cj-1,ci)+d(ci,cj+1))

移 位操作move :移位操作相當於選擇(Or2opt)操作,它將解序列中第i個元素ci移動到第j個元素cj之后的位置上。ΔD移位=(d(ci- 1,ci)+d(ci,ci+1)+d(cj,cj+1))-(d(ci-1,ci+1)+d(cj,ci)+d(ci,cj+1))

倒位操作(inverse) :倒位操作相當於選擇操作取r=2的情況,它將解序列中從第i個元素ci到第j個元素cj之間的元素的順序前后顛倒。倒位操作的性能指標為:

ΔD倒位=(d(ci-1,ci)+d(cj,cj+1))-(d(ci-1,cj)+d(ci,cj+1))

(4)如果部分解序列的長度k

 

歐拉路徑求解方法

法I:Fleury算法(深度優先遍歷)

數據結構:棧

int stk[1005];
int top;
int N, M, ss, tt;
int mp[1005][1005];

void dfs(int x) { //深度優先遍歷
    stk[top++] = x;
    for (int i = 1; i <= N; ++i) {
        if (mp[x][i]) {
            mp[x][i] = mp[i][x] = 0; // 刪除此邊
            dfs(i);
            break;
        }    
    }
}

void fleury(int start) {
    bool brige;
    top = 0; //top永遠指向下一個要入棧元素的存放位置
    stk[top++] = start; // 將起點放入Euler路徑中
    while (top > 0) {
        brige = true; //割邊(橋,最后一條連通外界的邊)也已經遍歷了
        for (int i = 1; i <= N; ++i) { // 遍歷節點
            if (mp[stk[top-1]][i]) { //如果與棧頂節點有邊
                brige = false;
                break;
            }
        }
        if (brige) { // 如果沒有點可以擴展,輸出並出棧,下一個while循環的時候會搜索下一個棧頂元素的其他路徑
            printf("%d ", stk[--top]);
        } else { // 否則繼續搜索歐拉路徑
            dfs(stk[--top]);
        } //從dfs返回,說明從節點stk[top-1]開始的深度遍歷已結束,下面找與它連通的下一個節點(廣度遍歷)。
    }
}

int main() {
    int x, y, deg, num;
    while (scanf("%d %d", &N, &M) != EOF) {
        memset(mp, 0, sizeof (mp));
        for (int i = 0; i < M; ++i) {
            scanf("%d %d", &x, &y);
            mp[x][y] = mp[y][x] = 1;
        }
        for (int i = 1; i <= N; ++i) { //計算節點度數,判斷是否符合歐拉路徑/歐拉回路的條件
            deg = num = 0;
            for (int j = 1; j <= N; ++j) {
                deg += mp[i][j];    
            }
            if (deg % 2 == 1) {
                start = i, ++num; //設置起始點
                printf("%d\n", i);
            }
        }
        if (num == 0 || num == 2) {
            fleury(start);
        } else {
            puts("No Euler path");
        }
    }
    return 0;    
}

 


免責聲明!

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



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