递归与动态规划

本文探讨了使用递归和动态规划来解决矩阵最小路径和、换钱问题以及数组的最长递增子序列等经典问题。通过不同场景的案例,展示了如何巧妙运用这两种算法来找到最优解,帮助理解它们在解决实际问题中的核心思想。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

矩阵的最小路径和

给一个矩阵,从左上角走到右下角,只能向右或向下走,求和最小的路径。

#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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值