474. 一和零
给你一个二进制字符串数组
strs
和两个整数m
和n
。请你找出并返回
strs
的最大子集的长度,该子集中 最多 有m
个0
和n
个1
。如果
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
cpp
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
cpp
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
cpp
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);
}
};
总结
动态规划、递归、迭代的相似性是走了一步,然后重复这一步解决问题,实际上我们只需要知道怎么走好这重复的一步就可以了.
怎么走好这重复的一步,利用已知信息构造一般的f函数,由已知信息推导出答案,然后走一步,重复利用这个f函数.
结尾
最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。
同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。
谢谢您的支持,期待与您在下一篇文章中再次相遇!