算法筆記_019:背包問題(Java)


目錄

1 問題描述

2 解決方案

2.1 蠻力法

2.2 減治法

2.2.1 遞歸求解

2.2.2 非遞歸求解(運用異或運算)

2.3 動態規划法

 


1 問題描述

給定n個重量為w1w2w3...wn,價值為v1v2...vn的物品和一個承重為W的背包,求這些物品中最有價值的子集(PS:每一個物品要么選一次,要么不選),並且要能夠裝到背包。

附形象描述:就像一個小偷打算把最有價值的贓物裝入他的背包一樣,但如果大家不喜歡扮演小偷的角色,也可以想象為一架運輸機打算把最有價值的物品運輸到外地,同時這些物品的重量不能超出它的運輸能力。

 


2 解決方案

2.1 蠻力法

使用蠻力法解決包含n個物品的背包問題,首先得求出這n個物品的所有子集,對於每一個物品存在兩種情況:選中(在程序中用1表示),未選中(在程序中用0表示)。該n個物品的所有子集數數量為2^n。下面請看一個簡單示例:

 

此處,使用一個二維數組存放所有子集,數組的每一行代表一個子集,每一列代表一個具體物品。

package com.liuzhen.chapterThree;

public class Knapsack {
    
    public int maxSumValue = 0;        //定義滿足背包問題子集的最大承重所得的總價值,初始化為0
    /*
     * 數組A的行數為2^n,代表n個物品共有2^n個子集,列數為n。即每一行的排列為一個背包實例
     * 數組weight存放每個物品的具體重量
     * 數組value存放每個物品的具體價值
     * n代表共有n個物品
     * maxWeight表示背包最大承重量
     */
    public void bruteForce(int[][] A,int[] weight,int[] value,int n,int maxWeight){
        
        for(int i = 0;i < Math.pow(2, n);i++){  //總共有2^n個子集,需要進行2^n次循環,及數組A有2^n行
            int temp1 = i;
            for(int j = 0;j < n;j++){    //數組A有n列,每一列代表一個物品
                int temp2 = temp1%2;
                A[i][j] = temp2;
                temp1 = temp1/2;
            }
        }
        
        printArray(A,weight,value,maxWeight);
        
    }
    
    //輸出窮舉方案的背包實例的選擇物品(0代表不包含該物品,1表示包含該物品)的總重量及總價值,並輸出最優實例的總價值
    public void printArray(int[][] A,int[] weight,int[] value,int maxWeight){
        int len1 = A.length;         //二維數組的行數
        int len2 = A[0].length;      //二維數組的列數
        for(int i = 0;i < len1;i++){
            int tempWeight = 0;      //暫時計算當前選中背包實例物品的總重量,初始化為0
            int tempSumValue = 0;    //暫時計算當前選中背包實例物品的總價值,初始化為0
            for(int j = 0;j < len2;j++){
                System.out.print(A[i][j]+" ");
//                if(A[i][j] != 0)
//                    System.out.print(" 物品"+j);
                tempWeight += A[i][j]*weight[j];
                tempSumValue += A[i][j]*value[j];
            }
            System.out.print("\t"+"總重量為:"+tempWeight);
            if(tempWeight <= maxWeight)
                System.out.print("\t"+"總價值為:"+tempSumValue);
            else
                System.out.print("\t"+"不可行(超出背包最大承重)");
            if(tempWeight <= maxWeight && tempSumValue > maxSumValue)
                maxSumValue = tempSumValue;
            System.out.println();
        }
        System.out.println("窮舉查找得知,最優解的總價值為:"+maxSumValue);
    }
    
    public static void main(String[] args){
        Knapsack test = new Knapsack();
        int[][] A = new int[16][4];
        int[] weight = {7,3,4,5};
        int[] value = {42,12,40,25};
        test.bruteForce(A,weight,value,4,10);      //背包的承重最大為10
    }
    
}

運行結果:

0 0 0 0     總重量為:0    總價值為:0
1 0 0 0     總重量為:7    總價值為:42
0 1 0 0     總重量為:3    總價值為:12
1 1 0 0     總重量為:10    總價值為:54
0 0 1 0     總重量為:4    總價值為:40
1 0 1 0     總重量為:11    不可行(超出背包最大承重)
0 1 1 0     總重量為:7    總價值為:52
1 1 1 0     總重量為:14    不可行(超出背包最大承重)
0 0 0 1     總重量為:5    總價值為:25
1 0 0 1     總重量為:12    不可行(超出背包最大承重)
0 1 0 1     總重量為:8    總價值為:37
1 1 0 1     總重量為:15    不可行(超出背包最大承重)
0 0 1 1 總重量為:9 總價值為:65
1 0 1 1     總重量為:16    不可行(超出背包最大承重)
0 1 1 1     總重量為:12    不可行(超出背包最大承重)
1 1 1 1     總重量為:19    不可行(超出背包最大承重)
窮舉查找得知,最優解的總價值為:65

 

2.2 減治法

2.2.1 遞歸求解

背包問題的實質是求取n個不同物品的所有子集,在此基礎上尋找重量合適,總價值最大的子集。此處只給出如何求出n個不同物品的所有子集實現,至於如何尋找符合背包問題的子集,感興趣的同學可以自己動手實現以下喲~

此處是運用減治法思想,根據二進制反射格雷碼的算法思想,來實現此問題。具體解釋,請看下面一段出自《算法設計與分析基礎》第三版上講解:

 

具體代碼如下:

package com.liuzhen.chapter4;

import java.util.LinkedList;
import java.util.List;

public class GrayCode {
    //遞歸求取n個不同物品的所有子集
    public String[] getGrayCode2(int n){
        int len = (int) Math.pow(2, n);
        String[] result = new String[len];
        if(n == 1){
            result[0] = "0";
            result[1] = "1";
            return result;
        }
        String[] temp = getGrayCode2(n-1);               //遞歸求取n-1個不同物品的所有子集
        for(int i = 0;i < temp.length;i++){        //根據格雷碼去掉最高位,前一半和后一半二進制數完全一樣的對稱性
            result[i] = "0" + temp[i];         //前一半格雷碼,最高位為0
            result[result.length-1-i] = "1" + temp[i];   //后一半格雷碼,最高位為1      
        }
        return result;
    }
    
    public static void main(String[] args){
        GrayCode test = new GrayCode();
        String[] temp2 = test.getGrayCode2(3);
        System.out.println("使用遞歸求解n個物品所有子集結果如下:");
        for(int i = 0;i < temp2.length;i++)
            System.out.println(temp2[i]);
    }
}

運行結果:

使用遞歸求解n個物品所有子集結果如下:
000
001
011
010
110
111
101
100

 

2.2.2 非遞歸求解(運用異或運算)

此處也使用求取格雷碼的思想,完成求取n個物品的所有子集,不過此處是使用非遞歸來實現,運用異或運算,其構造非常巧妙,個人感覺要理解這種編碼方式和思想得多多運用,直至熟能生巧。

具體代碼如下:

package com.liuzhen.chapter4;

import java.util.LinkedList;
import java.util.List;

public class GrayCode {
    //運用異或運算得到n個不同物品的所有子集
    public List<Integer> getGaryCode1(int n){
        List<Integer> result = new LinkedList<>();
        if(n >= 0){
            result.add(0);
            int top = 1;
            for(int i = 0;i < n;i++){
                System.out.print("result.size() = "+result.size()+"  ");
                for(int j = result.size()-1;j >= 0;j--){
                    System.out.print("result.get("+j+")^top = "+result.get(j)+"^"+top+" = "+(result.get(j)^top)+"  ");
                    result.add(result.get(j)^top);   //符號‘^’是異或運算(使用具體數字的二進制進行運算),即1^0=1,0^1=1,0^0=0,1^1=0
                }
                System.out.println();
                top <<= 1;         //top二進制左移1位,相當於top=top*2
                System.out.println("top = "+top);
            }
        }
        return result;
    }
    //把十進制數轉換成長度為n的二進制數
    public StringBuffer[] getBinary(List<Integer> A,int n){
        StringBuffer[] result = new StringBuffer[A.size()];
        for(int i = 0;i < A.size();i++){
            int temp1 = A.get(i);
            int judge = n;
            char[] temp2 = new char[n];     //用於存放temp1的n位二進制數
            while(judge > 0){
                int temp3 = temp1%2; 
                temp2[judge-1] = (char) (temp3+48);  //對照char的unicode編碼,把int型數字轉換為char型
                temp1 = temp1/2;
                judge--;    
            }
            result[i] = new StringBuffer(String.valueOf(temp2));
        }
        return result;
    }
    
    public static void main(String[] args){
        GrayCode test = new GrayCode();
        List<Integer> temp = test.getGaryCode1(3);
        System.out.println(temp);
        StringBuffer[] temp1 = test.getBinary(temp, 3);
        for(int i = 0;i < temp1.length;i++)
            System.out.println(temp1[i]);
    }
}

運行結果:

result.size() = 1  result.get(0)^top = 0^1 = 1  
top = 2
result.size() = 2  result.get(1)^top = 1^2 = 3  result.get(0)^top = 0^2 = 2  
top = 4
result.size() = 4  result.get(3)^top = 2^4 = 6  result.get(2)^top = 3^4 = 7  result.get(1)^top = 1^4 = 5  result.get(0)^top = 0^4 = 4  
top = 8
[0, 1, 3, 2, 6, 7, 5, 4]
000
001
011
010
110
111
101
100

 

2.3 動態規划法

此處編碼思想主要參考自《算法設計與分析基礎》第三版的一段講解,具體如下:

 

具體代碼如下:

package com.liuzhen.chapter8;

public class MFKnapsack {
    /*
     * 參數weight:物品1到物品n的重量,其中weight[0] = 0
     * 參數value:物品1到物品n的價值,其中value[0] = 0
     * 函功能:返回背包重量從0到所有物品重量之和區間的每一個重量所能達到的最大價值
     */
    public int[][] getMaxValue(int[] weight, int[] value) {
        int lenRow = weight.length;
        int lenColumn = 0;
        for(int i = 0;i < weight.length;i++)
            lenColumn += weight[i];
        int[][] F = new int[lenRow][lenColumn+1]; //列值長度加1,是因為最后一列要保證重量值為lenColumn  
        for(int i = 1;i < weight.length;i++) {
            for(int j = 1;j <= lenColumn;j++) {
                if(j < weight[i])
                    F[i][j] = F[i-1][j];
                else {
                    if(F[i-1][j] > F[i-1][j-weight[i]] + value[i])
                        F[i][j] = F[i-1][j];
                    else 
                        F[i][j] = F[i-1][j-weight[i]] + value[i];
                }
            }
        }
        return F;
    }
    
    public static void main(String[] args) {
        MFKnapsack test = new MFKnapsack();
        int[] weight = {0,2,1,3,2};
        int[] value = {0,12,10,20,15};
        int[][] F = test.getMaxValue(weight, value);
        System.out.println("背包承重從0到所有物品重量之和為8的承重能夠達到的最大價值分別為:");
        for(int i = 0;i < F.length;i++) {
            for(int j = 0;j < F[0].length;j++) 
                System.out.print(F[i][j]+"\t");
            System.out.println();
        }
    }
}

 

運行結果:

背包承重從0到所有物品重量之和為8的承重能夠達到的最大價值分別為:
0    0    0    0    0    0    0    0    0    
0    0    12    12    12    12    12    12    12    
0    10    12    22    22    22    22    22    22    
0    10    12    22    30    32    42    42    42    
0    10    15    25    30    37    45    47    57    

 

 

參考資料:

     1. java實現格雷碼生成

     2.背包問題九講

      3.《算法設計與分析基礎》第3版   Anany Levitin 著   潘彥 譯

 


免責聲明!

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



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