1. 程式人生 > >蘇蘇醬陪你學動態規劃(二)——合唱團

蘇蘇醬陪你學動態規劃(二)——合唱團

1、問題重述

     有 n 個學生站成一排,每個學生有一個能力值,牛牛想從這 n 個學生中按照順序選取 k 名學生,要求相鄰兩個學生的位置編號的差不超過 d,使得這 k 個學生的能力值的乘積最大,你能返回最大的乘積嗎?

2、題目分析  

     題目要求n各學生中選擇k個,使這k個學生的能力值乘積最大。這是一個最優化的問題。另外,在優化過程中,提出了相鄰兩個學生的位置編號差不超過d的約束。

如果不用遞迴或者動態規劃,問題很難入手,並且,限制條件d也需要對每一個進行約束,程式設計十分複雜

     所以,解決的方法是採用動態規劃(理由:1.求解的是最優化問題;2.可以分解為最優子結構)

     首先,對該問題的分解是關鍵。

從n個學生中,選擇k個,可以看成是:先從n個學生裡選擇最後1個,然後在剩下的裡選擇k-1個,並且讓這1個和前k-1個滿足約束條件

     其次,數學描述

為了能夠程式設計實現,需要歸納出其遞推公式,而在寫遞推公式之前,首先又需要對其進行數學描述

      記第k個人的位置為one,則可以用f[one][k]表示從n個人中選擇k個的方案。然後,它的子問題,需要從one前面的left個人裡面,選擇k-1個,這裡left表示k-1個人中最後一個(即第k-1個)人的位置,因此,子問題可以表示成f[left][k-1].

學生能力陣列記為arr[n+1],第i個學生的能力值為arr[i]
one表示最後一個人,其取值範圍為[1,n];
left表示第k-1個人所處的位置,需要和第k個人的位置差不超過d,因此
max{k-1,one-d}<=left<=one-1

      在n和k定了之後,需要求解出n個學生選擇k個能力值乘積的最大值。因為能力值有正有負,所以

當one對應的學生能力值為正時,
f[one][k] = max{f[left][k-1]arr[i]}(min{k-1,one-d}<=left<=one-1);
當one對應的學生能力值為負時
f[one][k] = max{g[left][k-1]
arr[i]}(min{k-1,one-d}<=left<=one-1);
此處g[][]是儲存n個選k個能力值乘積的最小值陣列

3、程式設計實現

import java.util.Scanner;

public class Main {
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        while(sc.hasNext()) {
            //總人數
            int n = sc.nextInt();
            //學生能力值陣列,第i個人直接對應arr[i]
            int[] arr = new int[n + 1];
            //初始化
            for (int i = 1; i <= n; i++) {//人直接對應座標
                arr[i] = sc.nextInt();
            }
            //選擇的學生數
            int kk = sc.nextInt();
            //間距
            int dd = sc.nextInt();

            /**
             * 遞推的時候,以f[one][k]的形式表示
             * 其中:one表示最後一個人的位置,k為包括這個人,一共有k個人
             * 原問題和子問題的關係:f[one][k]=max{f[left][k-1]*arr[one],g[left][k-1]*arr[one]}
             */
            //規劃陣列
            long[][] f = new long[n + 1][kk + 1];//人直接對應座標,n和kk都要+1
            long[][] g = new long[n + 1][kk + 1];
            //初始化k=1的情況
            for(int one = 1;one<=n;one++){
                f[one][1] = arr[one];
                g[one][1] = arr[one];
            }
            //自底向上遞推
            for(int k=2;k<=kk;k++){
                for(int one = k;one<=n;one++){
                    //求解當one和k定的時候,最大的分割點
                    long tempmax = Long.MIN_VALUE;
                    long tempmin = Long.MAX_VALUE;
                    for(int left = Math.max(k-1,one-dd);left<=one-1;left++){
                        if(tempmax<Math.max(f[left][k-1]*arr[one],g[left][k-1]*arr[one])){
                            tempmax=Math.max(f[left][k-1]*arr[one],g[left][k-1]*arr[one]);
                        }
                        if(tempmin>Math.min(f[left][k-1]*arr[one],g[left][k-1]*arr[one])){
                            tempmin=Math.min(f[left][k-1]*arr[one],g[left][k-1]*arr[one]);
                        }
                    }
                    f[one][k] = tempmax;
                    g[one][k] = tempmin;
                }
            }
            //n選k最大的需要從最後一個最大的位置選
            long result = Long.MIN_VALUE;
            for(int one = kk;one<=n;one++){
                if(result<f[one][kk]){
                    result = f[one][kk];
                }
            }
            System.out.println(result);
        }
    }
}

 本篇博文整理自牛客網菜鳥華的分享。