遞迴與動態規劃
阿新 • • 發佈:2019-01-02
矩陣的最小路徑和
給一個矩陣,從左上角走到右下角,只能向右或向下走,求和最小的路徑。
#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;
}