題目:給你一根長度為n的繩子,請把繩子剪成m段(m、n都是整數,n>1並且m>1),每一段的長度記為k[0],k[1],...k[m].請問k[0]xk[1]x...xk[m]可能 的最大乘積是多少?例如,當繩子的長度是8時,我們把它剪成長度分別為2、3、3的三段,此時得到的最大乘積是18.
我們有兩種不同的方法解決這個問題。先用常規的需要O(n^2)時間和O(n)空間的動態規划的思路,接着用只需要O(1)時間和空間的貪婪算法來分析解決這個問題。
動態規划
首先定義函數f(n)為把長度為n的繩子剪成若干段后各段長度乘積的最大值。在剪第一刀的時候,我們有n-1種可能的選擇,也就是剪出來的第一段繩子的可能長度為1,2,...n-1。因此f(n)=max(f(i)xf(n-i)),其中0<i<n.
這是一個從上至下的遞歸公式。由於遞歸會有很多重復的子問題,從而有大量不必要的重復計算。一個更好的辦法是按照從下而上的順序計算,也就是說我們先得到f(2)、f(3),再得到f(4)、f(5),直到得到f(n)。
當繩子的長度為2時,只可能剪成長度為1的兩段,因此f(2)等於1.當繩子的長度為3時,可能把繩子剪成長度為1和2的兩段或者長度都為1的三段,由於1x2>1x1x1,因此f(3)=2
int max(int length) { if (length < 2) { return 0; } if (length == 2) { return 1; } if (length == 3) { return 2; } int* array = new int[length + 1]; //如果length超過3,則2和3都可以直接作為一個段進行成績(不切割) array[0] = 0; array[1] = 1; array[2] = 2; array[3] = 3; int max = 0; for (int i = 4; i < length; i++) { max = 0; for (int j = 1; j < i / 2; j++)//因為對稱所以只需要計算一半就好 { int temp = array[j] * array[i - j]; if (max < temp) { max = temp; } array[i] = max; } } max = array[length]; delete[]array; return max; }
在上述代碼中,子問題的最優解存儲在數組array里。數組中第i個元素表示把長度為i的繩子剪成若干段之后各段長度乘積的最大值,即f(i)。我們注意到代碼中的第一個for循環變量i是順序遞增的,這意味着計算順序是自下而上的。因此,在求f(i)之前,對於每一個j(0<i<j)而言,f(j)都已經求解出來了,並且結果保存在array[j]里,為了求解f(i),我們需要求出所有可能的f(j)xf(i-j)並比較得出它們的最大值。這就是代碼中第二個for循環的功能。
int max(int length) { if (length < 2) { return 0; } if (length == 2) { return 1; } if (length == 3) { return 2; } //盡可能多地剪去長度為3的繩子 int temp = length / 3; //當繩子最后剩下長度為4的時候,不能再剪去長度為3的繩子段 //此時更好的方法是把繩子剪成長度為2的兩段,因此2x2>3x1 if (length - temp * 3 == 1) { temp -= 1; } //有三種情況,最后是0,1,2,3,4(0,1,3時temp=0,pow(2,0)=1)(2,4時pow分別為2和4) int temp2 = (length - temp * 3) / 2; return (int)(pow(3, temp))*(int)(pow(2, temp2)); }
接下來我們證明這種思路的正確性。首先,當n>=5的時候,我們可以證明2(n-2)>n並且3(n-3)>n。也就是說,當繩子剩下的長度大於或者等於5的時候,我們就把它剪成長度為3或者2的繩子段。另外,當n>=5時,3(n-3)>=2(n-2),因此我們應該盡可能地多剪長度為3的繩子段。
前面證明的前提是n>=5。那么當繩子的長度為4呢?在長度為4的繩子上剪一刀,有兩種可能的結果:剪成長度為1和3的兩根繩子,或者兩根長度都為2的繩子。注意到2x2>1x3,同時2x2=4也就是說,當繩子長度為4時其實沒有必要剪,只是題目的要求是至少要剪一刀。