- https://leetcode-cn.com/problems/path-with-minimum-effort/solution/dfs-er-fen-cha-zhao-mo-ban-ti-by-kobe24o/">https://leetcode-cn.com/problems/path-with-minimum-effort/solution/dfs-er-fen-cha-zhao-mo-ban-ti-by-kobe24o/
- ☆☆☆1631. 最小体力消耗路径">☆☆☆1631. 最小体力消耗路径
- ☆☆☆410. 分割数组的最大值">☆☆☆410. 分割数组的最大值
- 题目标题难度划分
- 算法掌握难度程度划分
https://leetcode-cn.com/problems/path-with-minimum-effort/solution/dfs-er-fen-cha-zhao-mo-ban-ti-by-kobe24o/
- 「使……最大值尽可能小」是二分搜索题目常见的问法。
- 「将数组分割为 m 段,求……」是动态规划题目常见的问法。
☆☆☆1631. 最小体力消耗路径
你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights ,其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左上角的格子 (0, 0) ,且你希望去最右下角的格子 (rows-1, columns-1) (注意下标从 0 开始编号)。你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。
一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。
请你返回从左上角走到右下角的最小 体力消耗值 。 示例 1:
输入:heights = [[1,2,2],[3,8,2],[5,3,5]] 输出:2 解释:路径 [1,3,5,3,5] 连续格子的差值绝对值最大为 2 。 这条路径比路径 [1,2,2,2,5] 更优,因为另一条路劲差值最大值为 3 。
二分法+dfs
思路
「使……最大值尽可能小」是二分搜索题目常见的问法。
本题中,我们注意到当选取一个mid
时,若矩阵中存在一条路径的体力<=mid
时,便设置右边界为mid-1
,保存mid
;若不存在,则说明矩阵中不存在任何一条路径存在该值,故设置左边界为mid+1
;
对于矩阵搜索中是否存在一条路径满足mid
,可采用DFS,对于某点(i,j)
,在其满足条件的方向上,若不存在体力值<mid
,则返回
;若存在一条,且最终该点可以走到(m-1,n-1)
,则返回true
;
算法
本题的难度在与如何剪枝,方法如下:
- 每次二分查找维护一个二维数组vis,保证所有探索过的路径不会经过第二次。
若在探索途中发现体力值>mid,则无须继续探索该方向,选取下一个方向(因为只要找到一条路径即可)
实现
class Solution {
vector<vector<int>> dir={{0,1},{0,-1},{1,0},{-1,0}};
int m,n;
public:
int minimumEffortPath(vector<vector<int>>& heights) {
m=heights.size();
n=heights[0].size();
int l=0,r=1e6,ans=0;
while(l<=r){
int mid=l+((r-l)>>1);
vector<vector<bool>> vis(m,vector<bool>(n,false));
if(dfs(heights,vis,0,0,mid)){
// if x existed,x<=mid。
r=mid-1;
ans=mid;
}
else{
//all x >mid
l=mid+1;
}
}
return ans;
}
bool dfs(vector<vector<int>>& heights,vector<vector<bool>>& vis,int x,int y,int tar){
if(x==m-1 && y==n-1){
return true;
}
vis[x][y]=true;
int i,j,k;
for(k=0;k<4;k++){
int i=x+dir[k][0];
int j=y+dir[k][1];
if(i>=0 && i<m && j>=0 && j<n
&& !vis[i][j]
&& abs(heights[i][j]-heights[x][y])<=tar){
if(dfs(heights,vis,i,j,tar)){
return true;
}
}
}
return false;
}
};
复杂度分析
时间复杂度:
- 空间复杂度:,vis所占用的空间。
☆☆☆410. 分割数组的最大值
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
注意: 数组长度 n 满足以下条件:
1 ≤ n ≤ 1000
1 ≤ m ≤ min(50, n)
示例:
输入:
nums = [7,2,5,10,8]
m = 2
输出:
18
解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
贪心+二分法
思路
「使……最大值尽可能小」是二分搜索题目常见的问法。
本题中,我们注意到:当我们选定一个值 mid,我们可以线性地验证是否存在一种分割方案,满足其最大分割子数组和不超过mid。策略如下:
针对mid,遍历数组,最大化每个子数组内元素的个数,保证sum(subArray)<=x;
- 若最大化子数组个数cnt<=m,说明依靠mid来分组,组数可能不满足,需要减少mid,故令右边界为mid
-
算法
设置左右边界: l=max(nums);r=sum(nums)
-
实现
class Solution {
public:
int splitArray(vector<int>& nums, int m) {
int l=0,r=0,ans=0;
for(auto val:nums){
if(l<val) l=val;
r+=val;
}
return binary(nums,m,l,r);
}
int binary(vector<int>& nums,int m,int l,int r){
int n=nums.size();
while(l<r){
int mid=l+((r-l)>>1);
int curSum=0,cnt=1;
for(int i=0;i<n;i++){
curSum+=nums[i];
if(curSum>mid){
cnt++;
curSum=nums[i];
}
}
if(cnt<=m){
r=mid;
}
else{
l=mid+1;
}
}
return l;
}
};
复杂度分析
时间复杂度:
-
动态规划
思路
「将数组分割为 m 段,求……」是动态规划题目常见的问法。
设置f[i][j]
为前i
个数分割为j段的和的最大值的最小值。现在考虑状态转移方式:
对于f[i][j]
,考虑j-1
段的问题,其为前k
个元素,则f[i][``j]
的值就取决于f[k][j-1]
与sub(k,i)
,其中sub(k,i)
为第j
段的和,故状态转移方程如下:
由于不能分出空的数组,限制条件为,对于i<j
的情况,由于要求出最小值,故将其设为一个很大的值。
算法
求解状态转移方程
- 对状态转移方程进行分析
-
实现
class Solution {
public:
int splitArray(vector<int>& nums, int m) {
int n=nums.size();
vector<vector<long long>> dp(n+1,vector<long long>(m+1,INT_MAX));
dp[0][0]=0;
vector<long long> segSum(n+1,0);
for(int i=0;i<n;i++){
segSum[i+1]=segSum[i]+nums[i];
}
for(int i=1;i<n+1;i++){
//i>=j
for(int j=1;j<=min(i,m);j++){
//k>=j-1
for(int k=j-1;k<i;k++){
dp[i][j]=min(dp[i][j],max(dp[k][j-1],segSum[i]-segSum[k]));
}
}
}
return dp[n][m];
}
};
复杂度分析
时间复杂度:
- 空间复杂度:
题目标题难度划分
星级 | 题目难度 |
---|---|
☆ | 简单 |
☆☆ | 中等 |
☆☆☆ | 困难 |
算法掌握难度程度划分
星级 | 掌握难度 |
---|---|
无 | 普通 |
❤ | 经典,易掌握 |
❤❤ | 经典,略难掌握 |
❤❤❤ | 困难 |