動態規劃刷題整理(持續更新)

(持續更新)

奇怪的漢諾塔(4柱漢諾塔)

描述
漢諾塔問題,條件如下:
1、這裏有A、B、C和D四座塔。
2、這裏有n個圓盤,n的數量是恆定的。
3、每個圓盤的尺寸都不相同。
4、所有的圓盤在開始時都堆疊在塔A上,且圓盤尺寸從塔頂到塔底逐漸增大。
5、我們需要將所有的圓盤都從塔A轉移到塔D上。
6、每次可以移動一個圓盤,當塔爲空塔或者塔頂圓盤尺寸大於被移動圓盤時,可將圓盤移至這座塔上。
請你求出將所有圓盤從塔A移動到塔D,所需的最小移動次數是多少。
輸入格式
沒有輸入
輸出格式
對於每一個整數n(1≤n≤12),輸出一個滿足條件的最小移動次數,每個結果佔一行。
輸入樣例:
沒有輸入
輸出樣例:
參考輸出格式

題目分析與解答

和經典的三柱子漢諾塔問題相比,這裏有四個柱子,所以四柱的漢諾塔問題一定是以三柱爲基礎的。
對於三柱漢諾塔問題,設hanno3[k]表示將k個盤子從第一柱移動到第三柱需要的移動次數,則有狀態轉移方程:

hanno3[k]=hanno3[k-1]*2+1

(解釋:先把前k-1個盤子移動到第二個柱子用hanno3[k-1]次,再把最下的圓盤移動到第三個柱子用1次,最後把前k-1個圓盤移動到第三個柱子。)
基於三柱的漢諾塔,對於四柱漢諾塔問題,設hanno4[k]表示將k個盤子從第一柱移動到第四柱需要的移動次數,則有狀態轉移方程:

hanno4[k]=min_{j=0,1,2,...,k-1}(hanno4[j]*2+hanno3[k-j])

(解釋:先把前j個放在第二個盤子需要hanno4[j]次;剩下的k-j個在除了第二個盤子的三個盤子上的移動構成三柱漢諾塔問題,將他們移動到第四柱,需要hanno3[k-j]次;最後將前j個放到第四個盤子需要hanno4[j]次。遍歷j取最小值。)
另外,對於三柱漢諾塔問題其實遞推方程有數學解:hanno3[k]=2^k-1,所以:

hanno4[k]=min_{j=0,1,2,...,k-1}(hanno4[j]*2+2^{k-j})=min_{j=0,1,2,...,k-1}(hanno4[k-j]*2+2^{j})

動態規劃代碼:

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int main() {
    int hanno4[20];
    memset(hanno4, 0x3f, sizeof hanno4);
    hanno4[0] = 0; hanno4[1] = 1;
    for (int i = 2; i <= 12; i++) {
        for (int j = 0; j < i; j++) {
            // hanno4[i] = min(hanno4[i], hanno4[j] * 2 + (1<<(i-j)) - 1);
            hanno4[i] = min(hanno4[i], hanno4[i-j] * 2 + (1<<j) - 1);
        }
    }
    for (int i = 1; i <= 12; i++)
        cout << hanno4[i] << endl;
    return 0;
}

Northwestern Europe 2002:Euro Efficiency

總時間限制: 1000ms 內存限制: 65536kB
描述
On January 1st 2002, The Netherlands, and several other European countries abandoned their national currency in favour of the Euro. This changed the ease of paying, and not just internationally.
A student buying a 68 guilder book before January 1st could pay for the book with one 50 guilder banknote and two 10 guilder banknotes, receiving two guilders in change. In short:50+10+10-1-1=68. Other ways of paying were: 50+25-5-1-1, or 100-25-5-1-1.Either way, there are always 5 units (banknotes or coins) involved in the payment process, and it
could not be done with less than 5 units.
Buying a 68 Euro book is easier these days: 50+20-2 = 68, so only 3 units are involved.This is no coincidence; in many other cases paying with euros is more efficient than paying with guilders. On average the Euro is more efficient. This has nothing to do, of course, with the value of the Euro, but with the units chosen. The units for guilders used to be: 1, 2.5, 5, 10, 25, 50,whereas the units for the Euro are: 1, 2, 5, 10, 20, 50.
For this problem we restrict ourselves to amounts up to 100 cents. The Euro has coins with values 1, 2, 5, 10, 20, 50 eurocents. In paying an arbitrary amount in the range [1, 100] eurocents, on average 2.96 coins are involved, either as payment or as change. The Euro series is not optimal in this sense. With coins 1, 24, 34, 39, 46, 50 an amount of 68 cents can be paid using two coins.The average number of coins involved in paying an amount in the range [1, 100] is 2.52.
Calculations with the latter series are more complex, however. That is, mental calculations.These calculations could easily be programmed in any mobile phone, which nearly everybody carries around nowadays. Preparing for the future, a committee of the European Central Bank is studying the efficiency of series of coins, to find the most efficient series for amounts up to 100 eurocents. They need your help.
Write a program that, given a series of coins, calculates the average and maximum number of coins needed to pay any amount up to and including 100 cents. You may assume that both parties involved have sufficient numbers of any coin at their disposal.
輸入
The first line of the input contains the number of test cases. Each test case is described by 6 different positive integers on a single line: the values of the coins, in ascending order. The first number is always 1. The last number is less than 100.
輸出
For each test case the output is a single line containing first the average and then the maximum number of coins involved in paying an amount in the range [1, 100]. These values are separated by a space. As in the example, the average should always contain two digits behind the decimal point. The maximum is always an integer.
樣例輸入

3
1 2 5 10 20 50
1 24 34 39 46 50
1 2 3 7 19 72

樣例輸出

2.96 5
2.52 3
2.80 4

題目分析與解答

分析
題目的大意是,給定一組6種不同面值的硬幣,每種硬幣個數不限,現在你拿着這些硬幣去購買價值爲total_value的東西,問參與交易的最少硬幣個數n是多少(可以多付錢,然後店家找錢),編程計算當total_value從1到100需要的最少硬幣個數求其均值,以及當total_value從1到100中最多需要的硬幣個數。
例子
看一個簡單的例子,爲了例子的直觀,現在假設只有三種面值的硬幣1,2,10,求total_value從1到10需要的最少硬幣個數均值,以及最多需要的硬幣個數。
total_value=1時,1張面值爲1的硬幣就夠了,min_count=1。
total_value=2時,1張面值爲2的硬幣就夠了,min_count=1。
total_value=3時,需要1張面值爲1的和1張面值爲2的,min_count=2。
total_value=4時,需要2張面值爲2的,min_count=2。
total_value=5時,需要2張面值爲2的和1張面值爲1的,min_count=3。
total_value=6時,需要3張面值爲2的,min_count=3。
total_value=7時,注意雖然可以3張面值2加1張面值1,這樣一共用4張;但是如果1張面值10的給店家,店家找1張面值2的和1張面值1的,交易只用了三張,因此是min_count=3。
total_value=8時,給1張面值10的,找一張面值2的,min_count=2。
total_value=9時,給1張面值10的,找一張面值1的,min_count=2。
total_value=10時,min_count=1。

total_value 1 2 3 4 5 6 7 8 9 10
min_count 1 1 2 2 3 3 3 2 2 1

於是需要的硬幣個數均值:
Ave = \frac {1+1+2+2+3+3+3+2+2+1}{10}=2

其中的最大值:
max\_use=3

問題的抽象
由上面的分析可以將問題抽象爲一個類完全揹包問題,用動態規劃解決。設硬幣面值數組爲coins[6]。定義狀態memo[k]表示購買價值爲k的物品需要參與交易的最少硬幣數量。
不過由於存在可以多付錢然後再找錢的過程,揹包的容量並不是每次購買的物品的價值k,所以memo數組的空間一定要大於100。於是將動態規劃分爲兩個階段,第一個階段只考慮付錢而不找錢來計算memo[k],初始memo[0]=0,狀態轉移方程:
memo[k]=min(memo[k],memo[k-coins[j]]+1),j \in [0,6)

第二個階段來考慮找錢,比如我們的簡單例子中,在第一階段會求得7=2+2+2+1,用了4個硬幣,現在考慮多付錢然後找錢的過程,可以付8找1,付9找2,付10找3,由於店家找錢也是用硬幣組合爲需要找給買家的價值,所以其實在第一個階段求解過,於是狀態轉移方程:
memo[k] = min(memo[k], memo[k + coin[j]] + 1),j \in [0,6)

自此問題已經很清晰了,給出動態規劃代碼:

# include<cstdio>
# include<algorithm>
# include<cmath>
# include<iostream>
using namespace std;
int INF = 0x3f3f3f3f;
const int maxn = 1000; // 不能寫100,因爲有找錢會減,中間狀態可能大於100

int memo[maxn];// memo[k]
int main() {
    int n;
    // freopen("input.txt", "r", stdin);
    cin >> n;//scanf_s("%d", &n);
    int coin[10];
    for (int i = 0; i < n; i++) {
        for (int j = 1; j <= 6; j++)
            cin >> coin[j]; //scanf("%d", &coin[j]);
        memo[0] = 0; // 初始化
        for (int j = 1; j < maxn; j++)
            memo[j] = INF;
        // 動態規劃
        for (int j = 1; j <= 6; j++)
            for (int k = coin[j]; k <= maxn; k++)
                if (memo[k - coin[j]] != INF)
                    memo[k] = min(memo[k], memo[k - coin[j]] + 1);
        
        for (int j = 1; j <= 6; j++)
            for (int k = maxn - coin[j]; k >= 0; k--)
                if (memo[k + coin[j]] != INF)
                    memo[k] = min(memo[k], memo[k + coin[j]] + 1);
        int sum = 0, max_count = -1;
        for (int j = 1; j <= 100; j++) {
            sum += memo[j];
            max_count = max(max_count, memo[j]);
        }
        double ave = (double)sum / 100;
        printf("%.2lf %d\n", ave, max_count);
    }
    // fclose(stdin);
    return 0;
}

LeetCode322號題目零錢兌換 - 力扣(LeetCode) 和該題目幾乎一樣。
(這兩個題目的兩個for循環可以換位置,不影響求解邏輯)

硬幣組合

leetcode硬幣組合

題目分析與解答

分析
顯然的完全揹包問題,所以可以對硬幣面值做循環,設f(i,k)表示使用coins[0,i]使得硬幣總量爲k的組合數,則有狀態轉移方程:

f(i,k)=f(i-1,k)+f(i,k-coins[i])

其含義將求解劃分爲了兩個不相交情況的並,如果要求使用coins[0,i]使得硬幣總量爲k的組合數,可以先計算只使用coins[0,i-1](即不使用coins[i])使得硬幣總量爲k的組合數,再加上使用coins[i]的組合數(至少使用coins[i]一次,所以是k-coins[i])。
解答

class Solution {
public:
    vector<int>coins = {1,5,10,25};
    int waysToChange(int n) {
        vector<vector<int>>f = vector<vector<int>>(coins.size(), vector<int>(n+1));
        for(int k=0;k<=n;k++)f[0][k] = 1;
        // for(int i=0; i<coins.length; i++) f[i][0] = 1; // 該語句可以省略,求解邏輯中會包含
        for(int i = 1;i < coins.size();i++){
            for(int k=0; k <= n; k ++){// 
                f[i][k] = f[i-1][k];
                if(k - coins[i] >= 0)
                    f[i][k] = (f[i-1][k] + f[i][k-coins[i]]) % 1000000007;
                    // f[i][k] += f[i][k-coins[i]];
            }
        }
        return f[coins.size()-1][n];
    }
};

進一步,如果只關注與求解有關的狀態:

class Solution {
public:
    
    int coins[4] = {1, 5,25,10};
    int waysToChange(int n) {
        vector<int>f(n+1, 0);
        f[0] = 1;
        for(int c: coins){
            for(int i=1; i <= n;i++)
                if(i>=c) f[i] = (f[i-c] + f[i]) % 1000000007;
        }     
        return f[n];
    }
};

LeetCode518零錢兌換

這道題目與上面不同是coins是指定的,所以不一定可以得到amount的數值,上題則是本題的特例。基本求解思路不變,只需要在進行初始化時,注意 f[0][k],在k不整除coins[0]時置0即可。

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        if(amount==0) return 1;
        if(coins.size()==0) return 0;
        vector<vector<int>>f = vector<vector<int>>(coins.size(), vector<int>(amount+1, 0));
        // for(int i=0;i<coins.size();i++) f[i][0] = 1; // 該語句可以省略
        for(int k=0;k<=amount;k++) f[0][k] = k % coins[0]==0 ? 1:0;       
        for(int i = 1;i < coins.size();i++){
            for(int k=0; k <= amount; k ++){// 
                f[i][k] = f[i-1][k];
                if(k - coins[i] >= 0)
                    f[i][k] = (f[i-1][k] + f[i][k-coins[i]]);   
            }
        }
        return f[coins.size()-1][amount];
    }
};
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int>f(amount+1, 0);
        f[0] = 1;
        for(int c: coins){
            for(int i=0;i<=amount;i++)
                if(i-c>=0)
                    f[i] += f[i-c];
        }
        return f[amount];
    }
};

(這兩個題目的for循環不可以換位置。)

和爲 K 的最少斐波那契數字數目

LeetCode雙週賽題目

題目分析:根據題意,首先要求得斐波那契數列的一些項,由題目規模的限制,大概求到46項即可。然後問題就轉化爲一個完全揹包問題,使用動態規劃。
題目解答:

class Solution {
public:
    int fib[51];
    void calfib() {
        fib[0] = 0; fib[1] = 1;
        for (int i = 2; i < 46; i++) 
            fib[i] = fib[i - 1] + fib[i - 2];       
    }
    int findMinFibonacciNumbers(int k) {
        int res = 0, pos = 45;
        calfib();
        while (k) {
            for (int i = pos; i >= 1; i--) {
                if (fib[i] <= k) {
                    k -= fib[i];
                    res++;
                    pos = i;
                    break;
                }
            }
        }
        return res;
    }
};

跳躍遊戲

跳躍遊戲LeetCode45

題目分析:根據題意,對於給定的數組numsnums[i]的值表示處在位置i能跳躍的最大距離,設
memo[k]表示第k步最遠能到達的位置

於是有狀態轉移方程:
memo[k+1] = max(j+nums[j], memo[k] + nums[memo[k]]),(for:j \leq memo[k])

其中memo[k-1]之前的必然已經被求解過,故可優化到:for:memo[k-1] \leq j \leq memo[k]
題目解答:

class Solution {
public:
// memo[i+1] = (j<=memo[i])max(j+nums[j], memo[i] + nums[memo[i]])
    vector<int>memo;
    int jump(vector<int>& nums) {
        if(nums.size()<=1)return 0;
        memo = vector<int>(nums.size()+1, 0);
        memo[0] = 0;
        int cur=0;
        for(int i=0;i<nums.size();i++){
            
            for(int j=cur;j<=memo[i];j++)
                memo[i+1] = max(j+nums[j],memo[i+1]);
            cur = memo[i];
            
            if(memo[i+1] >= nums.size()-1) return i+1;    
        }
        return memo[0];
    }
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章