【七十五】【算法分析与设计】474. 一和零,879. 盈利计划,688. 骑士在棋盘上的概率,三维动态规划

474. 一和零

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 输出:4 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

输入:strs = ["10", "0", "1"], m = 1, n = 1 输出:2 解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

提示:

  • 1 <= strs.length <= 600

  • 1 <= strs[i].length <= 100

  • strs[i] 仅由 '0''1' 组成

  • 1 <= m, n <= 100

 
class Solution {
public:
    // 定义一个字符串数组用于存储输入的字符串
    vector<string> strs;
    // 定义两个整数分别表示0和1的最大数量限制
    int m;
    int n;
    // 定义一个整数表示字符串数组的大小
    int size;
    // 定义一个三维动态规划数组用于存储中间结果
    vector<vector<vector<int>>> dp;
    
    // 初始化动态规划数组的辅助函数
    void solveinit() {
        size = strs.size(); // 更新字符串数组的大小
        dp.clear(); // 清空之前的动态规划数组
        // 重新初始化动态规划数组的大小为字符串数组的大小+1,以及m和n+1
        dp.resize(size, vector<vector<int>>(n + 1, vector<int>(m + 1, -1)));
    }
    
    // 深度优先搜索的递归函数,用于求解问题
    int dfs(int i, int n, int m) {
        // 如果n或m小于0,说明无法使用当前字符串,返回-1
        if (n < 0 || m < 0)
            return -1;
        // 如果i小于0,说明已经考虑完所有字符串,返回0
        if (i < 0)
            return 0; 
        // 如果当前状态的值已经在dp数组中,直接返回该值
        if (dp[i][n][m] != -1)
            return dp[i][n][m];
        
        int a = 0, b = 0;
        // 遍历当前字符串,统计0和1的数量
        for (const auto& x : strs[i]) {
            if (x == '1')
                a++;
            else
                b++;
        }
        // 递归求解不包含当前字符串和包含当前字符串的情况,取最大值
        dp[i][n][m] = max(dfs(i - 1, n, m), 1 + dfs(i - 1, n - a, m - b));
        
        // 返回当前状态的最优解
        return dp[i][n][m];
    }
    
    // 主函数,用于计算最大子集的长度
    int findMaxForm(vector<string>& _strs, int _m, int _n) {
        // 更新输入的字符串数组、m和n的值
        strs = _strs, m = _m, n = _n;
        // 初始化动态规划数组
        solveinit();
        // 调用dfs函数,从最后一个字符串开始向前求解
        return dfs(size - 1, n, m);
    }
};

879. 盈利计划

集团里有 n 名员工,他们可以完成各种各样的工作创造利润。

i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。如果成员参与了其中一项工作,就不能参与另一项工作。

工作的任何至少产生 minProfit 利润的子集称为 盈利计划 。并且工作的成员总数最多为 n

有多少种计划可以选择?因为答案很大,所以 返回结果模 10^9 + 7 的值

示例 1:

输入:n = 5, minProfit = 3, group = [2,2], profit = [2,3] 输出:2 解释:至少产生 3 的利润,该集团可以完成工作 0 和工作 1 ,或仅完成工作 1 。 总的来说,有两种计划。

示例 2:

输入:n = 10, minProfit = 5, group = [2,3,5], profit = [6,7,8] 输出:7 解释:至少产生 5 的利润,只要完成其中一种工作就行,所以该集团可以完成任何工作。 有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2) 。

提示:

  • 1 <= n <= 100

  • 0 <= minProfit <= 100

  • 1 <= group.length <= 100

  • 1 <= group[i] <= 100

  • profit.length == group.length

  • 0 <= profit[i] <= 100

 
class Solution {
public:
    int n; // 员工总数
    int minProfit; // 最小利润要求
    vector<int> group; // 每种工作所需员工数的数组
    vector<int> profit; // 每种工作所能产生利润的数组
    vector<vector<vector<int>>> dp; // 动态规划数组,用于存储中间结果
    int size; // 工作种类的数量
    const int MOD = 1e9 + 7; // 定义一个大数作为模数,以防止结果过大

    // 初始化动态规划数组的辅助函数
    void solveinit() {
        size = group.size(); // 更新工作种类的数量
        dp.clear(); // 清空之前的动态规划数组
        // 重新初始化动态规划数组的大小为工作种类的数量+1,以及员工总数n和最小利润minProfit+1
        dp.resize(size, vector<vector<int>>(n + 1, vector<int>(minProfit + 1, -1)));
    }

    // 深度优先搜索的递归函数,用于求解问题
    int dfs(int i, int n, int minp) {
        // 如果员工数小于0,则无法完成任何工作,返回0
        if (n < 0) {
            return 0;
        }
        // 如果没有剩余员工,且不需要更多利润,则有一种方案,否则没有方案
        if (i < 0) {
            return minp <= 0 ? 1 : 0;
        }
        // 如果所需最小利润小于0,则重置为0
        if (minp < 0)
            minp = 0;
        // 如果当前状态已经在dp数组中计算过,则直接返回结果
        if (dp[i][n][minp] != -1) {
            return dp[i][n][minp];
        }
        int a = group[i]; // 当前工作所需员工数
        int b = profit[i]; // 当前工作所能产生利润
        // 选择当前工作,递归求解在剩余员工数n-a和剩余所需利润minp-b的情况下的方法数
        int p1 = dfs(i - 1, n - a, minp - b);
        // 不选择当前工作,递归求解在剩余员工数n和剩余所需利润minp的情况下的方法数
        int p2 = dfs(i - 1, n, minp);
        // 将两种情况的结果相加,并取模,更新当前状态的最优解
        dp[i][n][minp] = (p1 + p2) % MOD;
        return dp[i][n][minp];
    }

    // 主函数,用于计算盈利计划的数量
    int profitableSchemes(int _n, int _minProfit, vector<int>& _group,
                          vector<int>& _profit) {
        n = _n, minProfit = _minProfit, group = _group, profit = _profit;
        solveinit(); // 初始化动态规划数组
        // 从最后一个工作开始向前递归求解,初始员工数为n,初始所需最小利润为minProfit
        return dfs(size - 1, n, minProfit);
    }
};

688. 骑士在棋盘上的概率

在一个 n x n 的国际象棋棋盘上,一个骑士从单元格 (row, column) 开始,并尝试进行 k 次移动。行和列是 从 0 开始 的,所以左上单元格是 (0,0) ,右下单元格是 (n - 1, n - 1)

象棋骑士有8种可能的走法,如下图所示。每次移动在基本方向上是两个单元格,然后在正交方向上是一个单元格。

每次骑士要移动时,它都会随机从8种可能的移动中选择一种(即使棋子会离开棋盘),然后移动到那里。

骑士继续移动,直到它走了 k 步或离开了棋盘。

返回 骑士在棋盘停止移动后仍留在棋盘上的概率

示例 1:

输入: n = 3, k = 2, row = 0, column = 0 输出: 0.0625 解释: 有两步(到(1,2),(2,1))可以让骑士留在棋盘上。 在每一个位置上,也有两种移动可以让骑士留在棋盘上。 骑士留在棋盘上的总概率是0.0625。

示例 2:

输入: n = 1, k = 0, row = 0, column = 0 输出: 1.00000

提示:

  • 1 <= n <= 25

  • 0 <= k <= 100

  • 0 <= row, column <= n - 1

 
class Solution {
public:
    int n; // 棋盘的大小
    int k; // 骑士要进行的移动次数
    int row; // 骑士的初始行位置
    int column; // 骑士的初始列位置
    using p = pair<int, int>; // 定义一个pair类型用于表示移动的方向
    // 定义骑士的8种可能移动方向
    const vector<p> d = {{1, 2},  {2, 1},  {2, -1},  {1, -2},
                         {-1, 2}, {-2, 1}, {-2, -1}, {-1, -2}};
    // 定义一个三维动态规划数组,用于存储中间结果
    vector<vector<vector<double>>> dp;

    // 初始化动态规划数组的辅助函数
    void solveinit() {
        dp.clear(), // 清空之前的动态规划数组
        dp.resize(n + 1, // 重新初始化动态规划数组的大小为n+1
                   vector<vector<double>>(n + 1, vector<double>(k + 1, -1)));
    }

    // 深度优先搜索的递归函数,用于计算骑士留在棋盘上的概率
    double dfs(int i, int j, int k) {
        // 如果当前位置超出棋盘范围,则无法移动,返回0
        if (i < 0 || i >= n || j < 0 || j >= n)
            return 0;
        // 如果移动次数为0,则不需要移动,返回1
        if (k == 0) {
            dp[i][j][k] = 1;
            return 1;
        }
        // 如果当前状态已经在dp数组中计算过,则直接返回结果
        if (dp[i][j][k] != -1)
            return dp[i][j][k];

        double ans = 0; // 初始化答案为0
        // 遍历所有可能的移动方向
        for (auto& t : d) {
            int x = i + t.first; // 计算移动后的新行位置
            int y = j + t.second; // 计算移动后的新列位置
            // 将当前位置的概率累加,每次移动的概率是前一状态概率的1/8
            ans += dfs(x, y, k - 1) / 8;
        }
        // 存储当前状态的概率,并返回
        dp[i][j][k] = ans;
        return ans;
    }

    // 主函数,用于计算骑士留在棋盘上的概率
    double knightProbability(int _n, int _k, int _row, int _column) {
        n = _n, k = _k, row = _row, column = _column; // 更新棋盘大小、移动次数和初始位置
        solveinit(); // 初始化动态规划数组
        // 从初始位置开始递归计算k步后留在棋盘上的概率
        return dfs(row, column, k);
    }
};

总结

1.

动态规划、递归、迭代的相似性是走了一步,然后重复这一步解决问题,实际上我们只需要知道怎么走好这重复的一步就可以了.

2.

怎么走好这重复的一步,利用已知信息构造一般的f函数,由已知信息推导出答案,然后走一步,重复利用这个f函数.

结尾

最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。

同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。

谢谢您的支持,期待与您在下一篇文章中再次相遇!

  • 7
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

妖精七七_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值