1. 程式人生 > >遞迴與動態規劃

遞迴與動態規劃

矩陣的最小路徑和

給一個矩陣,從左上角走到右下角,只能向右或向下走,求和最小的路徑。

#include <bits/stdc++.h>
using namespace std;

// 普通的動態規劃,空間複雜度和時間複雜度均為O(N*M)
int minSumPath(vector<vector<int> > a) {
    if (a.size() == 0 || a[0].size() == 0)
        return 0;
    int row = a.size(), col = a[0].size();
    vector<vector
<int>
>
dp(row, vector<int>(col)); dp[0][0] = a[0][0]; for (int i = 1; i < row; ++i) { dp[i][0] = dp[i-1][0] + a[i][0]; } for (int j = 1; j < col; ++j) { dp[0][j] = dp[0][j-1] + a[0][j]; } for (int i = 1; i < row; ++i) { for (int j = 1; j < col; ++j) { dp[i][j] = min(dp[i-1
][j], dp[i][j-1]) + a[i][j]; } } return dp[row-1][col-1]; } // 只維護一個數組,每遍歷一層的時候更新陣列.時間複雜度為O(N*M),空間複雜度為O(min{N,M}) int minSumPath2(vector<vector<int> > a) { if (a.size() == 0 || a[0].size() == 0) return 0; int row = a.size(), col = a[0].size(); int more = max(row, col), less = min(row, col); vector
<int>
dp(less); dp[0] = a[0][0]; for (int i = 1; i < less; i++) dp[i] = dp[i-1] + (row > col ? a[0][i] : a[i][0]); for (int i = 1; i < more; i++) { dp[0] = dp[0] + (row > col ? a[i][0] : a[0][i]); for (int j = 1; j < col; j++) { dp[j] = min(dp[j-1],dp[j]) + (row > col ? a[i][j] : a[j][i]); } } return dp[less-1]; } int main(int argc, char const *argv[]) { vector<vector<int> > a {{1,3,5,9}, {8,1,3,4},{5,0,6,1},{8,8,4,0}}; cout << minSumPath(a) << endl; cout << minSumPath2(a) << endl; return 0; }

換錢的最少貨幣數

問題一:給定陣列arr中所有的值都代表一種面值的貨幣,每種面值的貨幣可以使用任意張。 再給定一個整數aim代表要找的錢,求組成aim的最少貨幣數

問題二:給定陣列arr中所有的值都代表一種面值的貨幣,每種面值的貨幣只可以使用一次。再給定一個整數aim代表要找的錢,求組成aim的最少貨幣數

#include <bits/stdc++.h>
using namespace std;

/*
      給定陣列arr中所有的值都代表一種面值的貨幣,每種面值的貨幣可以使用任意張。
      再給定一個整數aim代表要找的錢,求組成aim的最少貨幣數
*/

// 時間複雜度和空間複雜度均為O(N*aim),N為陣列長度
int minCoins(vector<int> a, int aim) {
    if (a.size() == 0 || aim < 0) return 0;
    int n = a.size();
    // dp[i][j]的含義:在可以任意使用a[0..i]貨幣的情況下,組成j所需的最小張數
    vector<vector<int> > dp(n, vector<int>(aim+1));
    for (int j = 1; j <= aim; ++j) {
        dp[0][j] = INT_MAX;  // 表示只用第一種貨幣,組成j所需的最小張數,不能組成設定為最大值MAX
        if (j - a[0] >= 0 && dp[0][j-a[0]] != INT_MAX) // 避免j-a[0]<0,作為陣列下標會溢位
            dp[0][j] = dp[0][j-a[0]]+1;
    }
    int left = INT_MAX;
    for (int i = 1; i < n; ++i) {
        for (int j = 1; j <= aim; j++) {
            left = INT_MAX;
            // 遞推公式:dp[i][j] = min(dp[i-1][j], dp[i][j-a[i]]+1);
            if (j - a[i] >= 0 && dp[i][j-a[i]] != INT_MAX)
                left = dp[i][j-a[i]]+1;
            dp[i][j] = min(left, dp[i-1][j]);
        }
    }
    return dp[n-1][aim] != INT_MAX ? dp[n-1][aim] : -1;
}

// 只維護一個dp陣列,空間壓縮。時間複雜度O(N*aim),空間複雜度(aim)
int minCoins2(vector<int> a, int aim) {
    if (a.size() == 0 || aim < 0) return 0;
    int n = a.size();
    // dp[i]的含義:組成錢數為i的最小張數
    vector<int> dp(aim+1);
    for (int i = 1; i <= aim; i++) {  // 之所以不從下標0開始,是因為dp[0]代表第一種貨幣,不能設定成最大值
        dp[i] = INT_MAX;
        if (i - a[0] >= 0 && dp[i-a[0]] != INT_MAX) // 如果a[0]=2,那麼dp[2]=1,dp[4]=2,dp[6]=3...
            dp[i] = dp[i-a[0]]+1;
    }
    int left = INT_MAX;
    for (int i = 1; i < n; ++i) {
        for (int j = 1; j <= aim; ++j) {
            left = INT_MAX;
            // 遞推公式:dp[j] = min(dp[j], dp[j-a[i]]+1);
            if (j-a[i] >= 0 && dp[j-a[i]] != INT_MAX)
                left = dp[j-a[i]]+1;
            dp[j] = min(left, dp[j]);
        }
    }
    return dp[aim] != INT_MAX ? dp[aim] : -1;
}

/*
    給定陣列arr中所有的值都代表一種面值的貨幣,每種面值的貨幣只可以使用一次。
    再給定一個整數aim代表要找的錢,求組成aim的最少貨幣數
*/

// 時間、空間複雜度O(N*aim),N為陣列長度
int minCoins3(vector<int> a, int aim) {
    if (a.size() == 0 || aim < 0) return 0;
    int n = a.size();
    // dp[i][j]的含義:任意使用a[0..i]貨幣的情況下(每個只能用一次),組成j的最小張數
    vector<vector<int> > dp(n, vector<int>(aim+1));
    for (int i = 1; i <= aim; ++i) {  // 因為只能使用一次,所以只有dp[0][a[0]]為1,其餘均設定成最大值MAX
        dp[0][i] = INT_MAX;
    }
    if (a[0] <= aim) { // 如果a[0]=2,那麼能找開的錢僅為2,令dp[0][2]=1
        dp[0][a[0]] = 1;
    }
    int leftup = 0;
    for (int i = 1; i < n; ++i) {
        for (int j = 1; j <= aim; j++) {
            leftup = INT_MAX;
            // 遞推公式: dp[i][j] = min(dp[i-1][j], dp[i-1][j-a[i]]+1);
            if (j - a[i] >= 0 && dp[i-1][j-a[i]] != INT_MAX)
                leftup = dp[i-1][j-a[i]]+1;
            dp[i][j] = min(leftup, dp[i-1][j]);
        }
    }
    return dp[n-1][aim] != INT_MAX ? dp[n-1][aim] : -1;
}

// 時間複雜度O(N*aim) 空間複雜度O(aim)
int minCoins4(vector<int> a, int aim) {
    if (a.size() == 0 || aim < 0) return 0;
    int n = a.size();
    // dp[i]的含義:組成錢數為i的最小張數
    vector<int> dp(aim+1);
    for (int i = 1; i <= aim; i++)
        dp[i] = INT_MAX;
    if (a[0] <= aim)
        dp[a[0]] = 1;
    int leftup = 0;
    for (int i = 1; i < n; ++i) {
        for (int j = 1; j <= aim; j++) {
            leftup = INT_MAX;
            // 遞推公式:dp[j] = min(dp[j], dp[j-a[i]]+1);
            if (j - a[i] >= 0 && dp[j-a[i]] != INT_MAX)
                leftup = dp[j-a[i]]+1;
            dp[j] = min(leftup, dp[j]);
        }
    }
    return dp[aim] != INT_MAX ? dp[aim] : -1;
}

int main(int argc, char const *argv[]) {
    vector<int> a {5,2,3};
    cout << minCoins(a, 20) << endl;
    cout << minCoins2(a, 20) << endl;
    cout << minCoins3(a, 10) << endl;
    cout << minCoins4(a, 10) << endl;
    return 0;
}

換錢的方法數

給定陣列a,每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張。再給定aim代表要找的錢,求換錢有多少種方法

#include <bits/stdc++.h>
using namespace std;

// 暴力遞迴方法
// 如果用a[index..n-1]這些面值的錢組成aim,返回的總方法數
int process1(vector<int> &a, int index, int aim) {
    int res = 0;
    if (index == a.size()) {
        res = aim == 0 ? 1 : 0; // 目標錢數為0,返回值為1,表示各種面值的貨幣都使用0張
    } else {
        for (int i = 0; a[index]*i <= aim; i++)
            res += process1(a, index+1, aim-a[index]*i);
    }
    return res;
}
int coins1(vector<int> a, int aim) {
    if (aim < 0 || a.size() == 0) return 0;
    return process1(a, 0, aim);
}

// 記憶搜尋:對暴力遞迴做了進一步的優化。時間複雜度O(N*aim^2)
// 準備好全域性遍歷map,記錄已經計算過的遞迴過程的結果,防止下次重複計算。
int process2(vector<int> &a, int index, int aim, vector<vector<int> > &map) {
    int res = 0;
    if (index == a.size()) {
        res = aim == 0 ? 1 : 0;
    } else {
        int mapValue = 0;
        for (int i = 0; a[index]*i <= aim; i++) {
            mapValue = map[index+1][aim-a[index]*i];
            if (mapValue == 0) { // 因為初始化為0,所以0表示沒計算過
                res += process2(a, index+1, aim-a[index]*i, map);
            } else {  // 如果mapValue值為-1,那麼曾經計算過且返回值為0
                res += mapValue == -1 ? 0 : mapValue;
            }
        }
    }
    // 如果返回值res為0,則標記mapValue值為-1
    map[index][aim] = res == 0 ? -1 : res;
    return res;
}
int coins2(vector<int> a, int aim) {
    if (aim < 0 || a.size() == 0) return 0;
    vector<vector<int> >  map(a.size()+1, vector<int>(aim+1));
    return process2(a, 0, aim, map);
}

// 動態規劃方法:行數為N,列數為aim+1的矩陣dp
// dp[i][j]表示使用a[0..i]貨幣的情況下,組成錢數j有多少種方法

int coins3(vector<int> a, int aim) {
    if (aim < 0 || a.size() == 0) return 0;
    vector<vector<int> > dp(a.size(), vector<int>(aim+1));
    // 組成錢數為0的方法都是1
    for (int i = 0; i < a.size(); i++)
        dp[i][0] = 1;
    // 只用第一種貨幣(倍數)的方法為1
    for (int i = 1; a[0]*i <= aim; i++)
        dp[0][a[0]*i] = 1;
    int num = 0;
    for (int i = 1; i < a.size(); i++) {
        for (int j = 1; j <= aim; j++) {
            num = 0;
            // 遞推公式:dp[i][j] = dp[i-1][j-a[i]*k]
            for (int k = 0; j-a[i]*k >= 0; k++)
                num += dp[i-1][j-a[i]*k];
            dp[i][j] = num;
        }
    }
    return dp[a.size()-1][aim];
}

// 時間複雜度為O(N*aim)
int coins4(vector<int> a, int aim) {
    if (aim < 0 || a.size() == 0) return 0;
    vector<vector<int> > dp(a.size(), vector<int>(aim+1));
    // 組成錢數為0的方法都是1
    for (int i = 0; i < a.size(); i++)
        dp[i][0] = 1;
    // 只用第一種貨幣(倍數)的方法為1
    for (int i = 1; a[0]*i <= aim; i++)
        dp[0][a[0]*i] = 1;
    for (int i = 1; i < a.size(); i++) {
        for (int j = 1; j <= aim; j++) {
            // 遞推公式:dp[i][j] = dp[i-1][j] + dp[i][j-a[i]]
            dp[i][j] = dp[i-1][j];
            dp[i][j] += j-a[i] >= 0 ? dp[i][j-a[i]] : 0;
        }
    }
    return dp[a.size()-1][aim];
}

// 空間壓縮,時間複雜度為O(N*aim),空間複雜度O(aim)
int coins5(vector<int> a, int aim) {
    if (aim < 0 || a.size() == 0) return 0;
    vector<int> dp(aim+1);
    for (int i = 0; a[0]*i <= aim; i++)
        dp[a[0]*i] = 1;
    for (int i = 1; i < a.size(); i++) {
        for (int j = 1; j <= aim; j++) {
            dp[j] += j-a[i] >= 0 ? dp[j-a[i]] : 0;
        }
    }
    return dp[aim];
}

int main(int argc, char const *argv[]) {
    ios::sync_with_stdio(false);
    vector<int> v {5,10,25,1};
    cout << coins1(v, 25) << endl;
    cout << coins2(v, 25) << endl;
    cout << coins3(v, 25) << endl;
    cout << coins4(v, 25) << endl;
    cout << coins5(v, 25) << endl;
    return 0;
}

創造新世界

輸入x,n,m,分別表示x個物品,n個0,m個1。
接下來x行輸入1和0的字串,分別表示第i個物品需要多少個0和1。
求在僅給n個0和m個1的情況下,最多能建立多少個物品。

#include <bits/stdc++.h>
using namespace std;

/*
    動態規劃 dp[i][j] 表示用i個0和j個1能建立的最多物品數
*/

vector<string> *l;

int solve(int i, int numZeros, int numOnes) {
    vector<string> &list = *l;
    // 遞迴的終止條件
    if (i == list.size()-1) {
        for (int j = 0; j < list[i].size(); ++j) {
            if (list[i][j] == '1') --numOnes;
            if (list[i][j] == '0') --numZeros;
        }
        return ((numZeros | numOnes) >= 0) ? 1 : 0;
    }
    // 不建立當前item
    int a = solve(i+1, numZeros, numOnes);
    // 建立當前item
    for (int j = 0; j < list[i].size(); ++j) {
        if (list[i][j] == '1') --numOnes;
        if (list[i][j] == '0') --numZeros;
    }
    // 如果不能建立當前item,則直接返回a,否則返回a和b的最大值
    if ((numZeros | numOnes) < 0) return a;
    int b = 1 + solve(i+1, numZeros, numOnes);
    return max(a, b);
}

int main(int argc, char const *argv[]) {
    int x, n, m;
    vector<string> v;
    cin >> x >> n >> m;
    for (int i = 0; i < x; i++) {
        string tmp;
        cin >> tmp;
        v.push_back(tmp);
    }
    l = &v;
    cout << solve(0, n, m) << endl;
    return 0;
}

陣列的最長遞增子序列

#include <bits/stdc++.h>
using namespace std;

/*
    給定陣列a,返回a的最長遞增子序列
    如a=[2,1,5,3,6,4,8,9,7],返回的最長遞增子序列為[1,3,4,8,9]
*/

// 時間複雜度O(n^2) dp[i]表示在以a[i]這個數結尾的情況下,a[0..i]中的最大遞增子序列長度
vector<int> list1(vector<int> a) {
    if (a.size() == 0) return vector<int> {};
    vector<int> dp(a.size());
    // 第一步:求出dp陣列 O(n^2)
    for (int i = 0; i < a.size(); i++) {
        dp[i] = 1;
        for (int j = 0; j < i; j++) {
            if (a[j] < a[i])
                dp[i] = max(dp[i], dp[j]+1);
        }
    }
    // 第二步:根據dp陣列得到最長遞增子序列 O(n)
    int len = 0, index = 0;
    for (int i = 0; i < dp.size(); ++i) { // 先找出最大值以及下標位置
        if (dp[i] > len) {
            len = dp[i];
            index = i;
        }
    }
    vector<int> res(len);
    res[--len] = a[index];
    for (int i = index-1; i >= 0; i--) {
        // 滿足如下公式
        if (a[i] < a[index] && dp[i]+1 == dp[index]) { // 不斷尋找次大值及下標位置
            res[--len] = a[i];
            index = i;
        }
    }
    return res;
}

// 時間複雜度O(nlogn)
vector<int> list2(vector<int> a) {
    if (a.size() == 0) return vector<int> {};
    vector<int> dp(a.size());
    vector<int> ends(a.size());
    // 第一步:求出dp陣列 O(nlogn)
    dp[0] = 1;
    ends[0] = a[0];
    int l, m, r, right;
    l = m = r = right = 0;
    // ends[0..right]為有效區。如果有ends[b]=c,則表示遍歷到目前為止,
    // 在所有長度為b+1的遞增序列中,最小的結尾數為c
    for (int i = 1; i < a.size(); i++) {
        l = 0;
        r = right;
        while (l <= r) {  // 二分查詢在陣列中找>=a[i]的數
            m = (l+r)/2;
            if (ends[m] < a[i]) {
                l = m+1;
            } else {
                r = m-1;
            }
        }
        // 沒有找到>=a[i]的數時,l就比right大,更新最右邊界right的值
        right = max(right, l);
        // l為在ends陣列中>=a[i]的位置下標,並替換內容為a[i]
        ends[l] = a[i];
        // dp[i]表示在以a[i]這個數結尾的情況下,a[0..i]中的最大遞增子序列長度
        dp[i] = l+1;
    }
    // 第二步:根據dp陣列得到最長遞增子序列 O(n)
    int len = 0, index = 0;
    for (int i = 0; i < dp.size(); ++i) {
        if (dp[i] > len) {
            len = dp[i];
            index = i;
        }
    }
    vector<int> res(len);
    res[--len] = a[index];
    for (int i = index-1; i >= 0; i--) {
        // 滿足如下公式
        if (a[i] < a[index] && dp[i]+1 == dp[index]) {
            res[--len] = a[i];
            index = i;
        }
    }
    return res;
}

int main(int argc, char const *argv[]) {
    vector<int> a {2,1,5,3,6,4,8,9,7};
    vector<int> res = list1(a);
    for (auto i : res)
        cout << i <<  " ";
    cout << endl;
    vector<int> res2 = list2(a);
    for (auto i : res2)
        cout << i <<  " ";
    cout << endl;
    return 0;
}