0 废话
这周集中刷了点题,准备每周总结一下,先占个坑。周末主要任务是
- 写完上周作业
- 搞明白没懂的知识
- 周六晚参加一下双周赛
- 周日上午参加这周的周赛
- 概率论感觉没有复习的那么透,做点题?
- 总结这周题型和知识点
那么周日晚上见啦 !
哎…本来打算九点就开始写这篇的,结果知乎又给我推送了一个保研的帖子,就又把心仪的学校成功录取的学长学姐们的保研帖子看了一遍。呜呜呜太难过了,感觉更没戏了,哎,而且真的还差好多知识,我准备要继续加油了! 最后能去哪个学校也就随缘吧55555
1 动态规划
1.1 打家劫舍系列 House Robber I II III
1.1.1 House Robber I
题目:有一排房子,不能连续偷,求最大盗窃金额
思路:第 i 个房子的最大盗窃金额取决于前一个房子偷了没偷,如果前一个没偷,今天可以偷,反之则不行。两种情况:
- 前一个没偷:那就是现在第i个可以偷,也就是前前 i - 2 的最大盗窃金额 + 第 i 个房子的。
- 前一个偷了:就是现在这个不可以偷,也即是前一个 i - 1 房子的最大盗窃金额。
转移方程:dp[i] = max(dp[i -2] + dp[i], dp[i - 1]);
初始条件:dp[0] = nums[0]; dp[1] = max(nums[0], nums[1]);
代码:
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 0)
return 0;
else
{
vector<int> dp(n);
dp[0] = nums[0];
if(n == 1)
return dp[0];
else if(n == 2)
{
return max(nums[0], nums[1]);
}
else
{
dp[1] = max(nums[0], nums[1]);
for(int i = 2; i < n; i++)
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
return dp[n -1];
}
}
return 0;
}
};
先到这,我回宿舍洗澡,十点半双周赛 !
———————————————————————————————————
back
1.1. 2 House Robber II
题目 :在 I 的基础上,将房子变成了环状,也就是开头结尾不能一起偷。
思路 :在 I 分为两种情况,取两种情况的max
-
不偷开头的房子 :那么结尾的房子就可以偷,偷取范围是[1, n - 1]
-
dp1[1] = nums[1]; dp1[2] = max(nums[1], nums[2]);
-
for(int i = 3; i < n; i++) dp1[i] = max(dp1[i - 2] + nums[i], dp1[i -1];
-
不偷结尾的房子 :那么开头的房子可以偷,偷取范围是[0, n- 2]
-
dp2[0] = nums[0]; dp2[1] = max(nums[0], nums[1]);
-
for(int i = 2; i < n - 1; i++) dp2[i] = max(dp2[i - 2] + nums[i], dp2[i -1];
代码 :
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 1)
return nums[0];
if(n == 2)
return max(nums[0], nums[1]);
vector<int> dp1(n);
vector<int> dp2(n);
dp1[1] = nums[1];
dp1[2] = max(nums[1], nums[2]);
for(int i = 3; i < n; i++)
dp1[i] = max(dp1[i - 2] + nums[i], dp1[i - 1]);
dp2[0] = nums[0];
dp2[1] = max(nums[0], nums[1]);
for(int i = 2; i < n - 1; i++)
dp2[i] = max(dp2[i - 2] + nums[i], dp2[i - 1]);
//cout << dp1[n - 1] << " " << dp2[n - 2];
return max(dp1[n - 1], dp2[n - 2]);
}
};
1.1.3 House Robber III
题目:现在房子变成了二叉树(真厉害),直接相连的还是不能偷。
思路:
一开始的错误思路:对二叉树进行层序遍历,求出每层的和,再按照 I 的dp进行求解。但是在[2,1,3,null,4]这个错误case,错误思路求解结果是6 = 2 + 4,但结果是7 = 4 + 3。好吧,这个偷了右子树,是可以偷左子树的孩子的,这两个不是直接相连的。然后就不会了,看了题解。
正确思路:对二叉树进行dfs,对于当前节点总是有两种情况:
- 一种是偷当前节点,那就不可以偷他的左子树和右子树,但是可以偷他左子树的孩子和右子树的孩子。
- 另一种则是不偷当前节点,那就可以偷他的左子树和右子树,但是不能偷左子树的孩子和右子树的孩子。
统计这两种情况下的max,作为该节点所能偷窃的最大金额。子节点陆续将最大金额向上汇报,知道在根节点处返回。
优化:记忆化搜索,我们知道计算当前节点的时候,总会计算到子节点,孙子节点。我们可以把已经计算的值用hashmap保存一下,当我们计算儿子节点时,就可以直接使用下面的孙子节点值。
代码:
class Solution {
public:
unordered_map<TreeNode*, int> hashMap;
int rob(TreeNode* root) {
if(root == nullptr) return 0;
if(root->left == nullptr && root->right == nullptr) return root->val;
if(hashMap[root]) return hashMap[root];
int robVal = root->val;
if(root->left)
robVal += rob(root->left->left) + rob(root->left->right);
if(root->right)
robVal += rob(root->right->left) + rob(root->right->right);
int noRobVal = rob(root->left) + rob(root->right);
hashMap[root] = max(robVal, noRobVal);
return max(robVal, noRobVal);
}
};
1.2 不同路径 I II III
1.2.1 Unique Path I
题目:一个人站在左上角,想要去右下角,问有多少条不同路径?
思路:当前位置的路径种类来自上方位置路径种类和左侧路径的种类。
初始化条件:边界都是1,为了方便全都初始化为1
转移方程: dp[i][j] = dp[i - 1][j] + dp[i][j -1];
代码:
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 1));
for(int i = 1; i < m; i++)
for(int j = 1; j < n; j++)
dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
return dp[m - 1][n - 1];
}
};
排列组合:由于向下走向右走几步都是确定的,直接列公式就行
1.2.2 Unique Path II
题目:在 I 的基础上加入了障碍
思路:若当前位置加入障碍,那么当前位置的路径种类就变成了0,转移方程啥的都一样。但是,还有一点不一样,如果障碍在边界上,需要预先处理一下,障碍前的不变,还是1,障碍之后的边界线上都为0。这道题还有一个坑是障碍放在出发点,也是无语了。。。
代码:
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
if(obstacleGrid[0][0] == 1 || obstacleGrid[m - 1][n - 1] == 1)
return 0;
//处理边界上的障碍
for(int j = 0; j < n; j++)
{
if(obstacleGrid[0][j] == 0)
obstacleGrid[0][j] = 1;
else
{
while(j < n)
{
obstacleGrid[0][j] = 0;
j++;
}
break;
}
}
//开始dp
for(int i = 1; i < m; i++)
{
if(obstacleGrid[i][0]== 0)
obstacleGrid[i][0] = 1;
else
{
while(i < m)
{
obstacleGrid[i][0] = 0;
i++;
}
break;
}
}
for(int i = 1; i < m; i++)
{
for(int j = 1; j < n; j++)
{
if(obstacleGrid[i][j])
obstacleGrid[i][j] = 0;
else
obstacleGrid[i][j] = obstacleGrid[i - 1][j] + obstacleGrid[i][j - 1];
}
}
return obstacleGrid[m - 1][n - 1];
}
};
1.2.3 Unique Path III(还没做留坑)
2 DFS 深度遍历搜索
原来上课的时候就只以为DFS是用来遍历树的,直到做了1774,才深深的体会到DFS,本来自己想暴力for循环的,发现真的不行,才慢慢了解到DFS遍历网格。那第一道题就写1774吧 !
2.1 最接近目标的甜点成本
题目: 两个数组,一个基料成本,必须选一种;一个辅料成本,随便选几种[0,…],但是每种最多两份。还有一个target,求最接近target的成本
思路:首先遍历基料成本数组,将target = target - baseCosts[i],然后就是选辅料了。辅料成本和toppingCosts[0] * k0 + toppingCosts[1] * k1 + .. + toppingCosts[n] * kn
,其中ki可以取0, 1, 2。这个可以通过dfs实现,每层增加一种辅料可选项,具体如下图:
代码:
class Solution {
public:
void dfs(int level, int tmp, const vector<int> &a, int &ans , int target)
{
if(level == a.size())
{
if(abs(tmp - target) < abs(ans - target) || (abs(tmp - target) == abs(ans - target) && tmp < ans))
ans = tmp;
return ;
}
for(int i = 0; i < 3; i++)
dfs(level + 1, tmp + i * a[level], a, ans, target);
}
int closestCost(vector<int>& baseCosts, vector<int>& toppingCosts, int target)
{
int ans = INT_MAX;
for(int i = 0; i < baseCosts.size(); i++)
dfs(0, baseCosts[i], toppingCosts, ans, target);
return ans;
}
};
2.2 岛屿问题
岛屿系列属于dfs遍历网格类型,这个系列也很有意思。感觉网格和图很像,但是网格有边界,需要处理一下。
2.2.1 岛屿数量
题目: 二维网格grid,1表示水,0表示陆地。岛屿是指被水包围的陆地,网格只能通过水平或者垂直方向连接。求岛屿数量
思路:遍历二维网格,如果是陆地就dfs遍历该陆地连接的所有陆地,标记为2(防止外面遍历时重复),有多少次dfs就有多少个岛屿。
代码:
class Solution {
public:
void dfs(vector<vector<char>>& grid, int i, int j)
{
if(i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size() || grid[i][j] != '1')
return;
grid[i][j] = '2';
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
dfs(grid, i - 1, j);
dfs(grid, i + 1, j);
}
int numIslands(vector<vector<char>>& grid) {
int ans = 0;
for(int i = 0; i < grid.size(); i++)
{
for(int j = 0; j < grid[0].size(); j++)
{
if(grid[i][j] == '1')
{
dfs(grid, i, j);
ans++;
}
}
}
return ans;
}
};
dfs能做那么层序遍历也可以做的,方法就是queue进队出队,主要思路还是一样的。
代码:
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
int ans = 0;
queue<pair<int, int>> q;
for(int i = 0; i < grid.size(); i++)
{
for(int j = 0; j < grid[0].size(); j++)
{
if(grid[i][j] == '1')
{
q.push({i, j});
grid[i][j] = '2';
ans++;
while(!q.empty())
{
pair<int, int> t = q.front();
int m = t.first;
int n = t.second;
q.pop();
if(m >= -1 && m < grid.size() - 1 && grid[m + 1][n] == '1')
{
q.push(pair{m + 1, n});
grid[m + 1][n] = '2';
}
if(m >= 1 && m < grid.size() + 1 && grid[m - 1][n] == '1')
{
q.push(pair{m - 1, n});
grid[m - 1][n] = '2';
}
if(n >= -1 && n < grid[0].size() - 1 && grid[m][n + 1] == '1')
{
q.push(pair{m, n + 1});
grid[m][n + 1] = '2';
}
if(n >= 1 && n < grid[0].size() + 1 && grid[m][n - 1] == '1')
{
q.push(pair{m, n - 1});
grid[m][n - 1] = '2';
}
}
}
}
}
return ans;
}
};
2.2.2 岛屿周长
题目:题目背景和2.2.1相同,不过这道题只有一个岛屿,求岛屿外面的周长。
思路:我发现如果某块陆地位于边界上,就可以加一条边,如果左边有水,可以加一条边,同理右边、上边、下边有水的画都可以加一条边。
代码:
class Solution {
public:
int islandPerimeter(vector<vector<int>>& grid) {
int ans = 0;
int m = grid.size();
int n = grid[0].size();
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
if(grid[i][j] == 1)
{
if(i == 0) //左边界
ans++;
if(i == m - 1) //右边界
ans++;
if(i - 1 >= 0 && grid[i - 1][j] == 0) // 左有水
ans++;
if(i + 1 < m && grid[i + 1][j] == 0) // 右有水
ans++;
if(j == 0)
ans++;
if(j == n - 1)
ans++;
if(j - 1 >= 0 && grid[i][j - 1] == 0)
ans++;
if(j + 1 < n && grid[i][j + 1] == 0)
ans++;
}
}
}
return ans;
}
};
下面这个@笨猪爆破组的思路也不错
2.2.3 岛屿的最大面积
题目:背景同上,求最大的岛屿面积
思路:有了前面的基础,就很简单了。还是dfs遍历,每次遍历求一个面积,最后求最大面积
代码:
class Solution {
public:
void dfs(vector<vector<int>>& grid, int& tmp, int i, int j)
{
if(i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size() || grid[i][j] != 1)
return;
grid[i][j] = 2;
tmp++;
dfs(grid, tmp, i, j + 1);
dfs(grid, tmp, i, j - 1);
dfs(grid, tmp, i - 1, j);
dfs(grid, tmp, i + 1, j);
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
int ans = 0;
int m = grid.size();
int n = grid[0].size();
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
if(grid[i][j] == 1)
{
int tmp = 0;
dfs(grid, tmp, i, j);
if(tmp > ans)
ans = tmp;
}
}
}
return ans;
}
};
2.3.4 被围绕的区域
题目:将被X围绕的区域O用X填充。这道题有问题的地方在于下面的解释,边界上不会被围绕,我一开始没理解这个啥意思,看了评论区才发现是说与边界上的O连通的区域都不需要用X覆盖。
思路: 先对边界上的O进行dfs,标记为P。然后剩下的网格中的O就是需要用X修改的了,然后再将P改回O即可。
代码:
class Solution {
public:
void dfs(vector<vector<char>>& board,int i, int j)
{
int m = board.size();
int n = board[0].size();
if(i < 0 || i >= board.size() || j < 0 || j >= board[0].size() || board[i][j] != 'O')
return ;
board[i][j] = 'P';
dfs(board, i, j - 1);
dfs(board, i, j + 1);
dfs(board, i - 1, j);
dfs(board, i + 1, j);
}
void solve(vector<vector<char>>& board)
{
int m = board.size();
int n = board[0].size();
for(int i = 0; i < n; i++)
{
if(board[0][i] == 'O')
dfs(board, 0, i);
if(board[m - 1][i] == 'O')
dfs(board, m - 1, i);
}
for(int i = 0; i < m; i++)
{
if(board[i][0] == 'O')
dfs(board, i, 0);
if(board[i][n - 1] == 'O')
dfs(board, i, n - 1);
}
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
{
if(board[i][j] == 'O')
board[i][j] = 'X';
if(board[i][j] == 'P')
board[i][j] = 'O';
}
}
};
不行了,我好累,晚上再写点吧,争取把他写完。我看了一下还有好多要写的啊 ! 😭