1. 程式人生 > >鋼條切割問題(深度優先搜尋_普通的遞迴)

鋼條切割問題(深度優先搜尋_普通的遞迴)

1. 問題描述:

Serling公司購買長鋼條,將其切割為短鋼條出售。切割工序本身沒有成本支出。公司管理層希望知道最佳的切割方案。
假定我們知道Serling公司出售一段長為i英寸的鋼條的價格為pi(i=1,2,…,單位為美元)。鋼條的長度均為整英寸。

| 長度i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| - | - | - | - | - | - | - | - | - | - |
價格pi | 1 | 5 | 8 | 16 | 10 | 17 | 17 | 20 | 24 | 30 |

鋼條切割問題是這樣的:給定一段長度為n英寸的鋼條和一個價格表pi(i=1,2,…n),求切割鋼條方案,使得銷售收益rn最大。
注意,如果長度為n英寸的鋼條的價格pn足夠大,最優解可能就是完全不需要切割

2. ① 根據題目的最優解的字眼我們就可以使用普通的遞迴,深度優先搜尋,記憶性的遞迴,動態規劃來進行解決,我們先採用普通的遞迴其實也叫作深度優先搜尋來進行解決,後面的時候再對程式進行改進使用動態規劃來進行解決

根據題目中的測試用例,從直覺上可以猜測要想組成切割長度為10的鋼條,那麼切割的最大價值可能是切成兩份長度為4和一份長度為二的鋼條那麼就可以組成最大價值37,經過相關的計算髮現這個方案的確是最優的方案

② 可以發現鋼條的相同的長度是可以進行重複切割的,對於當前的鋼條長度,假如我們可以切,那麼我們就切掉當前的鋼條,那麼此時剩下的可以切掉的鋼條的長度就相應地減少,可以切也可以不切,嘗試每一種的可能性這其實也就是深度優先搜尋演算法的核心了(嘗試每一種的鋼條的切法),對於可以進行重複切割,我們可以在for迴圈中進行深度優先搜尋(遞迴)

③ 理解題目之後可以發現這道題目其實跟部分和的那道題目是非常類似的,給出一個目標數字,通過可以選擇的數字來組成這個目標數字,那麼其中就涉及到很多的組合,最終求解出所有能夠組成目標數字的組合,但是兩道題目又有區別,那道題目是求解出能夠組成目標數字的組合,而這道題目是求解出最大價值,但是思路也是一樣的,在搜尋的過程中同樣看能不能構成目標長度,並且在搜尋的過程中記錄下中間的價值

⑤ 我們可以設定一個全域性變數max來記錄搜尋過程的最大值,噹噹前的遞迴結束之後那麼與當前的max進行比較,因為遞迴結束之後說明我們的鋼條已經不能夠再進行切割了,此時比較累加的價值與當前的max比較來決定是否更新max

迴圈結束之後那麼最後得到的全域性變數max一定是需要求解的最終的最大價值

3. 具體的程式碼如下:

import static java.lang.Math.max;
import java.util.Scanner;
public class Main{
    static int Max = 0;
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        //鋼條的長度
        int len = sc.nextInt();
        int n = len;
        int v[] = new int[n];
        for(int i = 0; i < n; i++){
            v[i] = sc.nextInt();
        }
        dfs(v, 0, n, len, 0);
        System.out.println(Max);
        sc.close();
    }

    private static void dfs(int v[], int cur, int n, int len, int V){
        for(int i = 1; i <= len; i++){
            int max = 0;
            if(len >= i){
                dfs(v, cur + 1, n, len - i, V + v[i - 1]);
                max = max(V + v[i - 1], Max);
            }
                Max = max;
        }
                return;
    }
}

假如我們不能夠判斷到底輸出的數字是不是最大的,那麼我們可以把其中組成最大價值的組合給記錄下來那麼就可以進行判斷了,因為搜尋的過程是每一次的組成都是動態變化的,這裡我們使用List來進行記錄,採用呼叫之後加入當前元素,呼叫結束刪掉加入的當前元素,那麼呼叫結束之後list儲存的元素就是當前呼叫結束的組合了,此時還需要一個輔助的一維陣列來進行記錄當搜尋的過程中最大值有更新的情況下,那麼此時我們應該更新這個陣列,其中還需要一個變數來記錄當前呼叫到哪一層了,方便陣列更新到哪一個的位置,並且記錄下這個位置那麼最後得到的這個位置我們就可以對陣列從頭到這個位置的輸出那麼就可以知道組合是否正確了

具體的程式碼如下:

import static java.lang.Math.max;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class Main{
    //長度i:   1 2 3 4 5 6 7 8 9 10
    //價格pi:  10 1 5 8 16 10 17 17 20 24 30  輸出37
    //測試資料: 10 1 4 2 3 5 6 2 3 1 6 輸出20
    //測試資料: 10 1 1 4 2 3 1 6 3 2 1輸出13
    //假如我們需要記錄中間的過程,看一下最大價值的由什麼元素組成的,可以使用一維陣列來進行記錄
    //下面是可以輸出中間的過程的
    static int Max = 0;
    static List<Integer> list = new ArrayList<Integer>();
    //記錄中間過程的一維陣列
    static int recArray[];
    static int rec = 0;
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        //鋼條的長度
        int len = sc.nextInt();
        int n = len;
        int v[] = new int[n];
        recArray = new int[n];
        for(int i = 0; i < n; i++){
            v[i] = sc.nextInt();
        }
        dfs(v, 0, n, len, 0);
        System.out.println(Max);
        //下面輸出的list是空的,因為加一個然後刪一個最後導致list中沒有元素了
        //System.out.println(list);
        for(int i = 0; i <= rec; i++){
            System.out.print(recArray[i] + " ");
        }
        sc.close();
    }

    private static void dfs(int v[], int cur, int n, int len, int V){
    //下面的return不用寫是因為有if條件的限制可以起到出口的作用if(cur == 0)return;
        for(int i = 1; i <= len; i++){
            int max = 0;
            //if語句起到剪枝和出口的作用
            if(len >= i){
                list.add(i);
                dfs(v, cur + 1, n, len - i, V + v[i - 1]);
                //System.out.println(V);
                max = max(V + v[i - 1], Max);
                if(max > Max){
                    //System.out.println(list);
                    for(int k = 0; k <= list.size() - 1; k++){
                        recArray[k] = list.get(k);
                    }
                        rec = list.size() - 1;
                }
                        list.remove(list.size() - 1);
            }
                //能夠執行到這裡說明鋼條已經一種切割方案已經結束了,此時應該更新最大值Max
                Max = max;
        }
                return;
    }
}

解決過程中需要注意的問題:

不需要額外地寫遞迴的出口,因為通過if語句進行剪枝那麼當if語句判斷的時候發現不能夠再搜尋下去了,那麼它自動停止搜尋,把下面的路都給剪斷了,所以不需額外的遞迴出口

②這裡還需要注意的是某些下標的問題,有的時候特別容易發生陣列的越界,在除錯程式的過程中最好輸出其中變化的量的值,以觀察相關的變數是否變化正確,通過輸出語句也可以觀察其中的變化過程從而判斷程式的某些下標是否正確書寫,相關的細節需要仔細進行斟酌測試才可以保證程式使正確的,因為要組成目標長度的時候有很多時候很多支路都給if條件給剪斷了,那麼就會大大降低搜尋的時間,所以來說在資料量不是特別大的情況下,程式總的來說不是特別耗時的

③看到題目的時候需要聯想一下題目的相關性,是不是之前已經解決過這樣的問題,我們是否可以使用之前的思路來進行程式程式碼的改變以達到我們解決問題的目的呢?這些都是在解決問題的時候需要考慮到的,我們在一開始考慮使用深度優先搜尋的時候洗好畫一下遞迴樹,可以幫助我們更好地理解呼叫的過程

④還有一個就是記錄中間過程的問題,在動態變化的過程中,我們可以使用List,Map這些長度可以進行變化的資料結構來進行記錄,採用呼叫後加入當前元素,呼叫結束刪掉當前加入的元素那麼list得到的就是當前呼叫結束儲存的中間結果,對於這個中間結果我們進行處理以達到我們的目的