1.kotori和n皇后

题意简单来说就是,在一个无穷大的棋盘上,不断插入k个皇后,皇后们如果在 同一行,同一列,在一个45°主对角线 在一个135°副对角线上,就可以互相攻击。
我们需要判断在第i个皇后插入后,是否存在互相攻击的情况。
解法:哈希表。
我们只需要将皇后的攻击范围存入哈希表(unordered_set)中,每次插入时判断这个皇后是否在这个攻击范围内即可。
因此我们需要创建四个哈希表。对应的行 列,两个对角线。
行和列很好理解,关于对角线

比如主对角线,如果y - x相等,说明在同一个主对角线上。同理,如果y + x相等,说明在同一个副对角线上。
最后,我们只需要记录下第一次出现皇后相互攻击的时机即可。
代码:
#include <iostream>
#include <unordered_set>
using namespace std;
int main()
{
int k,t;
cin >> k;
int ret = 0x3f3f3f3f;
unordered_set <int> row;
unordered_set <int> low;
unordered_set <int> dig1;// 主对角线
unordered_set <int> dig2;// 副对角线
int a,b;
for(int i = 0; i < k; ++i)
{
cin >> a >> b;
if(row.count(a) || low.count(b) || dig1.count(b - a) || dig2.count(b + a))
{
if(ret == 0x3f3f3f3f)
{
ret = i + 1;
}
}
else
{
row.insert(a);
low.insert(b);
dig1.insert(b - a);
dig2.insert(b + a);
}
}
cin >> t;
int tmp;
for(int i = 0; i < t; ++i)
{
cin >> tmp;
if(tmp >= ret) cout << "Yes" << endl;
else cout << "No" << endl;
}
return 0;
}
2.取金币
这是一道比较难的区间dp问题。
题意简单,图中很容易理解。
状态表示:
dp[i][j]表示区间[i,j]的金币全部拿走,能获得的最大积分是多少。
状态转移方程:

假设我们取了[i,j]区间的第k堆金币,那么此时区间[i,j]能获得的最大积分也就是dp[i][j] =
dp[i][k - 1] + dp[k + 1][j] + arr[k] * arr[i - 1] * arr[j + 1]。( i <= k <= j )
记得每次要取最大值。
然后我们再来考虑一下边界情况
首先是对于源数组的,我们可以给源数组的左右两边都新增一个元素1,这样可以在不影响计算的情况下防止数组越界。
然后对于dp表,我们可以多开两行和多开两列, 因为源数组我们新增了两个元素1,所以多开两行和两列可以避免填表时数组越界,dp表都初始化为0不影响计算结果。

并且我们的填表总是在对角线及以上填表的。因为 i >= j。
再来看填表顺序,跟以往不同,我们要对照着状态转移方程来看
dp[i][k - 1] + dp[k + 1][j] + arr[k] * arr[i - 1] * arr[j + 1]。( i <= k <= j )
我们发现填表需要用到j + 1,要用到i - 1,所以填表顺序为从下往上,从左往右填。
最后返回dp[1,n]即可。
代码:
class Solution {
public:
int arr[110];
int dp[110][110];
int getCoins(vector<int>& coins) {
int n = coins.size();
arr[0] = arr[n + 1] = 1;
for(int i = 1; i <= n; ++i) arr[i] = coins[i - 1]; // 新增两个元素
for(int i = n; i >= 1; --i)
{
for(int j = i; j <= n; j++)
{
for(int k = i; k <= j; ++k)
dp[i][j] = max(dp[i][j],dp[i][k - 1] + dp[k + 1][j] + arr[k] * arr[i - 1] * arr[j + 1]);
}
}
return dp[1][n];
}
};
3.四个选项
题意简单来说就是有四个选项,给出四个选项的数量(保证四个选项加起来是12个),要将这些选项填到12个空里面,同时存在m个额外条件,每个条件使得第x个选项要等于第y个选项。
解法:递归 dfs
剪枝有两点:
1.如果当前该选项已经没有数量了,应该剪枝。
2.如果填入该选项发现不满足额外条件,应该剪枝。
细节:
1.可以用cnt[5]数组来存选项的数量,并用下标1 2 3 4 来映射选项的A B C D。
2.用哈希的思想来存额外条件,可以用一个二维的bool类型矩阵来存。
3.可以用一个变长数组来存选项填入的顺序,并事先加入一个占位符,方便对应下标映射关系。
代码:
#include <iostream>
#include <vector>
using namespace std;
int cnt[5];
int m,x,y;
bool same[13][13]; // 记录x,y相等的情况
int ret;
vector<int>path; // 存放路径
bool isSame(int pos,int cur)
{
for(int i = 1; i < pos; ++i)
{
if(same[i][pos] && path[i] != cur) return false;
}
return true;
}
void dfs(int pos)
{
if(pos > 12)
{
ret++;
return;
}
for(int i = 1; i <= 4; ++i)
{
if(cnt[i] == 0) continue; // 剪枝1 该选项数量已为0
if(!isSame(pos,i)) continue; // 剪枝2 选项不相同
cnt[i]--;
path.push_back(i);
dfs(pos + 1);
cnt[i]++;
path.pop_back();
}
}
int main()
{
for(int i = 1; i <= 4; ++i) cin >> cnt[i];
cin >> m;
while(m--)
{
cin >> x >> y;
same[x][y] = same[y][x] = true;
}
path.push_back(0); // 先增一个占位符
dfs(1);
cout << ret << endl;
return 0;
}
4.接雨水
本题解法很多,这里采用动态规划(预处理)的解法。

思想:先求出每一根柱子上能接多少雨水,然后把所有柱子能接的雨水累加即可。
那么怎么计算第i根柱子能接多少雨水呢?我们发现,第i根柱子能接的雨水根取决与该柱子左边柱子的最大值和右边柱子的最大值,取二者的较小值就是该柱子能接的雨水量。
所有我们可以用两个数组,left[i],right[i]来记录第i根柱子左边柱子的最大值,和右边柱子的最大值。
也就是前缀最大值。
另外有一个细节:
比如left[i],这个数组表示的是区间[0,i]的最大值,要把i也带进去,因为我们计算每一个柱子的公式是 ret += min(left[i],right[i]) - height[i]; 如果没有把i算进去的话,恰好i柱子是最高的,那么结果就会产生负值,是不合理的,最少的雨水量是0。所以我们把i也算进去,才是合理的。
代码:
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
vector<int> left(n);
vector<int> right(n);
left[0] = height[0];
right[n - 1] = height[n - 1];
for(int i = 1; i < n; ++i)
left[i] = max(left[i - 1],height[i]);
for(int i = n - 2; i >= 0; --i)
right[i] = max(right[i + 1],height[i]);
int ret = 0;
for(int i = 0; i < n; ++i)
{
ret += min(left[i],right[i]) - height[i];
}
return ret;
}
};
5.栈和排序

解法:栈+贪心
贪心思想:优先让当前最大的值出栈。
先创建一个栈
1.依次进栈。
2. 先更新目标值
3.如果当前的栈顶元素大于目标值,说明它是当前能输出的最大值。
另外可以定义一个哈希表,来存元素是否入栈。
class Solution {
public:
vector<int> solve(vector<int>& a) {
int n = a.size();
stack<int> s;
int aim = n; // 目标值
vector<int> ret;
vector<bool> hash(n + 1);
for(auto x : a)
{
s.push(x);
hash[x] = true;
while(hash[aim]) // 先更新目标值
{
aim--;
}
while(s.size() && s.top() >= aim)
{
ret.push_back(s.top());
s.pop();
}
}
return ret;
}
};
6.加减

综合性很强的一道题。
题意简单来说就是给了一个数组,还有最多k次操作机会,每次操作可以使数组的某个元素+1或者-1, 在经过最多k次操作后,使得数组中出现最多相同的数的次数,求出这个元素出现的次数。
思路:
1.贪心思想:我们要尽可能的选择挨得近的数,这样把它们变成相同的数所花费的次数就会少。
那么我们可以先将数组进行排序,这样就可以让数组的元素尽可能挨得近。
2.之后,我们就可以枚举数组中所有的区间,找出使区间内所有数变成相同的数所花费的代价cost <= k,在这些区间中的最大区间。
如果直接暴力枚举,时间复杂度O(N^2),超时。
在枚举过程中,我们可以发现其left和right指针移动的单调性,所以可以用滑动窗口来优化,那么时间复杂度降为O(N)。
3.关于怎样计算区间的最小花费
这里引入一个数学问题:在一个数轴上有一些点,如何选取一个位置,使得所有的点到这个位置的距离之和最小?
答案是:选取中间的点,如果点的个数是偶数,那么中间任意两个点都可以。
所以将这个结论应用到这题

a1就是left,a5就是right所指向的位置,那么该区间的最少花费也就是所有点到a3的距离。
如果我们每次都要对每个点都计算的话,那么这里计算的时间复杂度为O(N),因此我们可以使用前缀和,把时间复杂度降为O(1)。
用sum数组代表前缀和数组。

原本求cost的公式化简后,下划线表示的是这个地方可以用前缀和来代替,并且发现了一个规律,其余的部分就是 (a3前有多少个点) * a3 - (a3后有多少个点) * a3 - (前缀和1) + (前缀和2)
那么此时最小cost的计算公式为:
这个公式对应了原本求cost化简后的例子。
并且发现其中还有可以化简的地方,我们在代码中体现了。
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
long long arr[N];
long long sum[N]; // 前缀和数组
long long n,k;
long long cal(int l,int r) // 计算区间的花费
{
int mid = l + (r - l) / 2;
return (2 * mid - l - r) * arr[mid] - (sum[mid - 1] - sum[l - 1]) + (sum[r] - sum[mid]);
}
int main()
{
cin >> n >> k;
for(int i = 1; i <= n; ++i) // 使用了前缀和,那么下标从1开始比较方便
cin >> arr[i];
sort(arr + 1,arr + n + 1); // 使得元素之间挨得近
for(int i = 1; i <= n; ++i) // 下标从1开始,可以防止越界
sum[i] = sum[i - 1] + arr[i]; // 前缀和
int left = 1,right = 1;
int ret = 1;
long long cost; // 区间的所需要花费的次数
while(right <= n)
{ // 进窗口
if(cal(left,right) <= k)
{
++right;
ret = max(ret,right - left);
}
else // 出窗口
{
++left;
}
}
cout << ret << endl;
return 0;
}
所以本题的时间复杂度为 O(N logN)主要是排序所消耗的时间。
运用了枚举 + 前缀和 + 滑动窗口 + 贪心。并且还有数学问题的推导,公式的化简。
7.mari和shiny

解法:动态规划
是多状态的线性dp。
其实只要把状态表示想明白就是一道比较简单的dp。
如果我们要找到能组成 ‘shy’的子序列,那么首先得知道有多少个 ‘sh’ 子序列,如果想知道有多少个 ‘sh’子序列,就需要知道有 多少个 's'。
那么就有三个状态,那么就存在三个dp表。
s[i] 表示在[0,i]区间有多少个 's'。
h[i] 表示在[0,i]区间有多少个 'sh'。
y[i]表示在[0,i]区间有多少个'shy'。
因为它的状态转移方程很简单,先填好s表,然后再填h表,最后再填y表,然后返回y表的结果。
所以就不多说了。三次for循环搞定。
另外有一个细节就是初始化,我们可以多开一个格子,方便填表。
还有就是本题数据量比较大,记得用 long long 类型来存。
代码:
#include <iostream>
using namespace std;
const int N = 3e5 + 10;
long long s[N],h[N],y[N];
int main()
{
int n;
string tmp;
cin >> n >> tmp;
for(int i = 1; i <= n; ++i)
{
s[i] = s[i - 1];
if(tmp[i - 1] == 's')
s[i] += 1;
}
for(int i = 1; i <= n; ++i)
{
h[i] = h[i - 1];
if(tmp[i - 1] == 'h')
h[i] += s[i];
}
for(int i = 1; i <= n; ++i)
{
y[i] = y[i - 1];
if(tmp[i - 1] == 'y')
y[i] += + h[i];
}
cout << y[n] << endl;
return 0;
}
8.城市群数量

一道比较简单的关于连通块的题。
需要注意的是:我们定义一个一维的vis数组,来标记这个城市是否遍历过,而不是二维的。
并且在dfs遍历移动的时候,也不是上下左右四个方向移动的。
代码:
class Solution {
public:
bool vis[210] = {0};
int n;
void dfs(vector<vector<int> >& m,int pos)
{
for(int i = 0; i < n; ++i)
{
if(!vis[i] && m[pos][i])
{
vis[i] = true;
dfs(m,i);
}
}
}
int citys(vector<vector<int> >& m) {
n = m.size();
int ret = 0;
for(int i = 0; i < n; ++i)
{
if(!vis[i])
{
dfs(m,i);
++ret;
}
}
return ret;
}
};
9.最大数
本题的解法是贪心。
贪心体现在排序上,我们需要指定一个排序规则,让大的在前面,小的在后面。我们可以配合sort函数,假设数组的每一个元素都是一个string类型,那么只需要满足 s1 + s2 > s2 + s1。这就是排序规则。
关于细节:

1.排序之所以能排序,是因为其中满足了传递性,比如 a > b,b > c可以推出 a > c。但是我们无法证明我们之前的排序规则具有传递性。(当然本道题其实是满足这个规则的)
2.关于优化问题,我们可以把每个元素都先预处理成string类型。
3.关于前导零问题,如果返回的最终结果是 "000",那么此时答案应该是 "0"。依据本题的比较规则,大的在后面,所以我们只需要判断ret[0]是否是"0"即可。
代码:
class Solution {
public:
string largestNumber(vector<int>& nums) {
vector<string> tmp;
for(auto x : nums) tmp.push_back(to_string(x));
sort(tmp.begin(),tmp.end(),[](const string& s1,const string& s2) {
return s1 + s2 > s2 + s1;
});
string ret;
for(auto& s : tmp)
{
ret += s;
}
if(ret[0] == '0') return "0";
return ret;
}
};
10.摆动序列
解法:贪心 : O(N),动态规划 : O(N ^ 2)
这里重点说贪心解法

我们可以从宏观上来看这个数组,我们发现只要统计极值点就可以了。
但是有一个需要注意的地方

如果碰见连续的相同的值,就不好判断这是否是一个峰顶还是山谷,或者它并没有符合摆动序列的性质。为了解决它可以定义一个left和right,对于i位置的点,left表示i的左边的上升或者下降的情况,right表示的就是右边的。如果 left * rihgt <= 0,说明这里是一个极值点。
另外,right = nums[i] - nums[i - 1],如果right == 0,那么就直接continue就行了。
每次循环最后需要记得更新left = right。
代码:
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int left = 0;
int ret = 1;
int n = nums.size();
for(int i = 0; i < n - 1; ++i)
{
int right = nums[i] - nums[i + 1];
if(right == 0) continue;
if(left * right <= 0) ++ret;
left = right;
}
return ret;
}
};
动态规划代码:
简单说下状态表示:
dp1[i]表示,以0到i区间,先下降,再上升的摆动序列的最长的长度。
dp2[i]表示,以0到i区间,先上升,再下降的摆动序列的最长的长度。
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n = nums.size();
vector<int> dp1(n,1);
vector<int> dp2(n,1);
int ret = 1;
for(int i = 1; i < n; i++)
{
for(int j = i - 1; j >= 0; j--)
{
if(nums[i] < nums[j])
dp1[i] = max(dp1[i],dp2[j] + 1);
else if(nums[i] > nums[j])
dp2[i] = max(dp2[i],dp1[j] + 1);
}
ret = max(dp1[i],dp2[i]);
}
return ret;
}
};
11.最长递增子序列
这道题先想出来的一般都是动态规划的解法。其中时间复杂度为 O(N^2),空间复杂度为O(N)。
简单说下动态规划的解法:
首先是状态表示: dp[i] 表示以i位置为结尾的最长递增子序列
状态转移方程: dp[i] = max(dp[i],dp[j] + 1) ( 0 <= j < i && nums[i] > nums[j] )
代码:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n,1);
int ret = 1;
for(int i = 1; i < n; i++)
{
for(int j = i - 1; j >= 0; j--)
if(nums[i] > nums[j])
dp[i] = max(dp[i],dp[j] + 1);
ret = max(ret,dp[i]);
}
return ret;
}
};
然后是贪心的解法:
首先由dp解法发现了一个规律,那就是我们只关心这个最长递增子序列的末尾,并不关系它之前长什么样。
于是我们可以定义一个数组

依次扫描原数组成员,我们只关心长度为多少的子序列的末尾元素是什么,比如数组的第一个元素是 7 ,那么当扫描到3的时候,我们发现3不能接在7后面形成递增子序列,那么就直接将7替换成3,因为后续如果有元素能接在7后面,那么就一定也能接在3后面,这就是该解法的贪心,用交换论证法即可证明。
接在在3后面扫描到了8,发现8能接在3后面,于是就把8放在长度为2的位置作为子序列的末尾。
二分优化查找:
在扫描到一个元素后,我们需要判断它适合放在长度数组中的哪一个位置,现在我们已经知道了,它应该放在第一次大于等于nums[i]的位置。
边界情况:
当长度数组为0,或者nums[i] 大于len[m - ]也就是最大值的时候,直接插入到长度数组后面。
可以用直接证明的方式证明长度数组的元素是递增的。
那么用二分法优化后,贪心的解法的时间复杂度为 O(N logN),空间复杂度为O(N)
代码
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> len;
int left,right,mid;
for(auto x : nums)
{
if(len.size() == 0 || x > len.back())
{
len.push_back(x);
}
else
{
left = 0;
right = len.size() - 1;
while(left < right)
{
mid = (right - left) / 2 + left;
if(len[mid] < x) left = mid + 1;
else right = mid;
}
len[left] = x;
}
}
return len.size();
}
};
12.优势洗牌
这道题用贪心的解法。
这道题的题意和田忌赛马的故事是一样的。贪心策略有两点:
1.尽可能的废物利用,用自己最差的牌去抵消掉对面最强的牌。
2.只用最小的差距去赢得对面的牌。
并且要注意:这道题我们需要先将两个数组排序

比如图中, num1是我方的牌。如果 8 小于 11,说明8也不可能比得过11后面的任意一张牌了。
于是直接让8这张牌与对面的32进行抵消。接着 12 大于 11,并且是num1中刚好大于 11。因此就用12去跟11配对。所以本题是需要先排序的。
注意事项,我们在排完序后,原先数组对应的下标映射关系是被打乱了的,因此我们在排序的时候,需要保存原来的下标映射关系。主要是针对nums2的,因为题意是要我们根据nums2的顺序选择出牌顺序。
那么我们在排序nums2的时候,可以不用真的排序nums2,而是排序nums2的下标。在配对比较的时候,用下标映射原来nums2元素的值即可。所以还需要定义一个映射nums2下标的数组,并且要初始化。在填写结果的时候,可以定义left 和 right ,最废物的牌从right往后开始放,可以成功的牌从left往前开始放。记得它们的移动方向。
代码:
class Solution {
public:
vector<int> advantageCount(vector<int>& nums1, vector<int>& nums2) {
int n = nums1.size();
vector<int> tmp(n); // nums2的映射下标数组
for(int i = 0; i < n; ++i) tmp[i] = i;
sort(tmp.begin(),tmp.end(),[&](const int n1,const int n2){
return nums2[n1] < nums2[n2]; // 按照nums2排序的下标
});
sort(nums1.begin(),nums1.end());
int left = 0,right = n - 1;
vector<int> ret(n);
for(int i = 0; i < n; ++i)
{
if(nums1[i] <= nums2[tmp[left]])
{
ret[tmp[right--]] = nums1[i];
}
else
{
ret[tmp[left++]] = nums1[i];
}
}
return ret;
}
};
13.跳跃游戏

解法有两种:
一.动态规划 时间复杂度 O(N ^ 2)
二. 贪心(层序遍历的思想) 时间复杂度 O(N)
本题动规可以通过,大约能击败 5%的用户,所以具有参考意义,但不是重点,这里简单说下:
1.状态表示 : dp[i] 表示 跳跃到 i 位置时,所需要的最小跳跃步数。
2.状态转移方程: 当遍历到 i 位置的时候, 再定义一个指针 j ,去遍历 i - 1 的区域,如果满足
(nums[j] + j) >= i 时,说明可以从j位置跳到i位置,
于是此时的状态转移方程 -> dp[i] = min(dp[i],dp[j] + 1)。
3.本题要注意初始化问题:因为每次dp[i]取的都是最小值,如果dp表里默认都是0的话,根本就没有比较的意义,因此需要把表中的值都初始化为一个很大的值(0x3f3f3f3f)。另外 dp[0]需要初始化为0。
4.返回值问题:但是注意本题,我们只需要计算到 n - 1的位置就可以停下来了,所以返回的是 dp[n - 1]。
代码:
class Solution {
public:
// dp版
int jump(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n + 1,0x3f3f3f3f);
dp[0] = 0;
for(int i = 1; i <= n; ++i)
{
for(int j = 0; j < i; ++j)
{
if(nums[j] + j >= i)
{
dp[i] = min(dp[i],dp[j] + 1);
}
}
}
return dp[n - 1];
}
};
层序遍历(贪心)解法:

可以定义left和right指针,表示每一次跳跃能够跳到的范围。
每次可以用left进行遍历,while(left <= right)。然后再定义一个变量 maxpos,表示在这一次遍历中,能移动的最大的位置,然后记得将right = maxpos 作为下一次的右区间。left则移动到 right + 1的位置即可。然后将遍历的层数将其返回即可。
代码:
class Solution {
public:
// 层序遍历思想(贪心)
int jump(vector<int>& nums) {
int n = nums.size();
int ret = 0;
int left = 0,right = 0,maxpos = 0;
while(right < n - 1)
{
++ret;
while(left <= right)
{
maxpos = max(maxpos,left + nums[left]);
++left;
}
right = maxpos;
}
return ret;
}
};
14.加油站

本题的解法:贪心。
这道题的第一印象是暴力解法,本题贪心就是在暴力解法的基础上进行优化的。
所以先说暴力解法:
我们可以用一个diff[i]数组,来表示i位置的盈余油量是多少,大于0说明能够到达,否则不能到达。

那么我们可以通过两边for循环:
第一层:遍历diff的每一个元素。
第二层:判断这个元素是否能走完一圈回到自己的位置。
这样的话时间复杂度为 O(N ^ 2)是会超时的。
贪心:
拿一个diff数组为例,我们发现

如果当枚举到 a 位置的时候,如果 汽车最多只能走到 f 位置 (a肯定是大于等于0的),也就是说 a + b + c + d + e + f 是小于0的,那么下标 i 在++之后, b + c + d + e + f也一定是小于0的,那么我们的 i 就不用仅仅只加1,而是直接跳到g的位置,再开始判断。i 的更新就变成了 i += step + 1,step表示刚刚移动了多少步。
最坏的情况就是,第一次 i 跳到了 h 的位置,然后 再进行了一次判断,如果还是不能走完一圈,那么 i 也会跳出循环,所以最差也是 2N 。
代码:
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n = gas.size();
vector<int> diff(n);
for(int i = 0; i < n; ++i) diff[i] = gas[i] - cost[i];
for(int i = 0; i < n; ++i)
{
if(diff[i] < 0) continue;
int step = 0;
int rest = 0;
for(;step < n; ++step)
{
int index = (step + i) % n;
rest += diff[index];
if(rest < 0) break;
}
if(rest >= 0) return i;
i += step;
}
return -1;
}
};
15.单调递增的数字
解法:暴力枚举和贪心
暴力枚举:
有从大到小的顺序 一次枚举 [0,n]区间的数字,每次都要判断这个数是否符合单调递增的数,返回第一次出现的满足题意的数字,也就是小于等于n的最大的数。
代码(会超时):
class Solution {
public:
bool func(int n)
{
string tmp = to_string(n);
for(int i = 1; i < tmp.size(); ++i)
{
if(tmp[i] < tmp[i - 1])
return false;
}
return true;
}
int monotoneIncreasingDigits(int n) {
while(!func(n)) --n;
return n;
}
};
贪心:

主要是找规律得出的,我们发现,遍历数组时,当第一次出现不符合递增的情况时,我们让它的前一个数字减一,然后让其后面所有的数都变成9,这样既符合单调递增,也符合小于等于n的最大数。
注意事项:
1.如果出现 5554这样5有很多重复值的时候,我们需要找到第一个5,然后将后面的数都变为9,如5554 -> 4999。
使用stoi函数转化结果的时候,它是会处理前导零问题的。
代码:
class Solution {
public:
int monotoneIncreasingDigits(int n) {
string s = to_string(n);
int sz = s.size();
int i = 0;
// 找到第一次递减时的位置,注意不要越界
while(i + 1 < sz && s[i] <= s[i + 1]) ++i;
if(i + 1 == sz) return n; // 特殊情况,这个n本身就是符合条件的
// 如果这一段都是相同的数,那么要找到这一段的第一个
while(i - 1 >= 0 && s[i] == s[i - 1]) --i;
s[i] = s[i] - 1;
for(int j = i + 1; j < sz; ++j) s[j] = '9';
return stoi(s); // 会处理前导零的
}
};
16.坏了的计算器
解法:贪心。
关于贪心的推导,我们可以先试试正向推导:

以3 转化到 10为例,如果此时的贪心策略是:begin 小于end,那么就 乘以2,大于就减去一,以图中的上面路径为例,结果是四次。但是我们发现,还存在这样一条路径 3 -> 6 -> 5 -> 10。此时的结果只有三次,也就是说我们刚刚的贪心策略是错误的。
至此我们发现,如果正向推导的话,对于一个数究竟是该用乘法还是减法是不好判断的,于是就有了解法二
正难则反:

既然由3 -> 10比较难,那么我们可以从 10 -> 3。那么此时的可以用的两种操作就变成了除以二和加上一。
并且我们发现,本题是没有小数的,也就是说 奇数除以二是除不进的。 那么此时对于一个数是除还是加就比较好定义了。
贪心策略:此时是 end -> begin 。 那么当 end > begin 时:
1.当end == 偶数 -> 除以二。
2.当end == 奇数 -> 只能加一。
当end < begin 时,一直加到 begin即可,其实直接加上 begin - end 就行了。
代码:
class Solution { public: int brokenCalc(int startValue, int target) { int ret = 0; while(target > startValue) { if(target % 2 == 0) // 偶数 target /= 2; else ++target; ++ret; } ret += startValue - target; return ret; } };
17.合并区间
解法:排序 + 贪心
对于区间问题,都可以先思考一下是否需要先排序。这里既可以排序数组的左端点,也可以排序右端点。
并且要注意这道题的示例2,这道题仅仅是点重合了也算重叠区间的。

我们发现这其实就是一个求并集的过程

在排序之后,假设当前遍历到的区间是 [left,right] 下一个区间是 [a,b],那么只要 当 a <= right 。就说明这两个区间是重叠的,符合合并条件。
这道题贪心的点就是:在排序之后 ,如果 区间 [left,right] 不与 [a,b]重叠,那么 [left,right] 与 [a,b]之后的区间都不会重叠了。
关于编写代码:我们可以创建一个ret结果数组,首先插入nums的第一个元素,后续从下标1位置开始遍历nums数组,用ret的最后一个元素去比较,如果符合合并条件,那么就更新ret的最后的元素的右区间 right = max(right,b)。否则就新尾插入一个元素。
代码:
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> ret;
int n = intervals.size();
sort(intervals.begin(),intervals.end()); // 先尾插一个元素
ret.push_back({intervals[0][0],intervals[0][1]});
for(int i = 1; i < n; ++i)
{
if(intervals[i][0] <= ret.back()[1]) // 更新
{
ret.back()[1] = max(ret.back()[1],intervals[i][1]);
}
else
{
ret.push_back({intervals[i][0],intervals[i][1]}); // 尾插
}
}
return ret;
}
};
18.无重叠区间

解法:排序 + 贪心
这里题意需要我们求移除区间的最小数量,但是我们可以换一种思路:保留区间的最大数量。
排完序后,当出现重叠区间的时候,此时的贪心策略就是:移除右端点较大的区间,这样剩下的区间可以尽量的避免与之后的区间重叠。注意这里跟合并区间那里不一样,这里当 a == right时不算重叠。代码:
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
vector<vector<int>>ret;
int n = intervals.size();
sort(intervals.begin(),intervals.end()); // 排序
ret.push_back({intervals[0][0],intervals[0][1]});
for(int i = 1; i < n; ++i)
{
if(intervals[i][0] < ret.back()[1]) // 重叠了
{
if(intervals[i][1] < ret.back()[1]) // 保留右端点较小的区间
{
ret.pop_back();
ret.push_back({intervals[i][0],intervals[i][1]});
}
}
else
{
ret.push_back({intervals[i][0],intervals[i][1]});
}
}
return n - ret.size();
}
};
19.用最少数量的箭引爆气球

题意看似复杂,其实就是给了一个数组,每一个元素代表一个气球的区间,我们可以用箭来引爆重叠区间的气球,求出最少需要多少箭来引爆所有气球。
解法:排序 + 贪心
仔细思考一下便发现,这道题和合并区间那里有点不一样,合并区间求的是并集,这里求的是交集

对于每一个区间的更新,我们依旧只需要更新右区间,取最小值即可,因为数组已经是排序过了的,所以右区间如果满足,左区间也一定满足。
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
int n = points.size();
sort(points.begin(),points.end()); // 排序
int right = points[0][1]; // 右端点
int ret = 1;
for(int i = 1; i < n; ++i)
{
if(points[i][0] <= right) // 重叠了
{
right = min(right,points[i][1]); // 取交集,更新右区间
}
else
{
right = points[i][1];
++ret;
}
}
return ret;
}
};
20.整数替换

解法:模拟(递归 + 记忆化搜索) || 贪心
模拟:
我们发现每一次对n的处理都是一个子问题,那么就只需要每次返回时先将结果记录到备忘录中,然后再返回即可。
代码:
class Solution {
public:
unordered_map<long long,int> memo; // 备忘录
long long dfs(long long n)
{
// 递归出口,当备忘录中有记录时,直接返回备忘录中的值。
if(memo.find(n) != memo.end())
{
return memo[n];
}
if(n % 2 == 0)
{
memo[n] = 1 + dfs(n / 2);
return memo[n];
}
else
{
memo[n] = min(dfs(n + 1),dfs(n - 1)) + 1;
return memo[n];
}
}
int integerReplacement(int n) {
memo[1] = 0; // 可以提前把1放入备忘录中
return dfs(n);
}
};
贪心:
首先复习一下二进制的操作

接下来就是分情况讨论了:
1.n为偶数:只能进行 / 2操作。
2.n为奇数:

a.当二进制的后两位为 ....01时:我们通过找规律发现,最优的处理方法是 减一。
b.当二进制的后两位为....11时:加一是最优方法。
c.特殊情况。当n == 3时,它的最优处理方法是减一,而且很容易就知道它仅需要两步。
到这里就可以写代码了,另外关于n已经是奇数时,判断它属于哪种情况的技巧,我们可以让 n & 3,这样就可以通过结果来判断是哪种情况了,或者也可以让 n % 4也行。
代码:
class Solution {
public:
int integerReplacement(long long n) {
int ret = 0;
while(n > 1)
{
if(n == 3) // 特殊情况
{
ret += 2;
break;
}
if((n & 1) == 0) // 偶数
{
n /= 2;
}
else
{
if((n & 3) == 1) n -= 1;
else n += 1;
}
++ret;
}
return ret;
}
};
21.俄罗斯套娃信封问题

解法: 动态规划(会超时) || 贪心 + 二分
动态规划:
虽然会超时,但是这个解法十分通用,同样值得学习。理解题意后,其实发现这跟最长递增子序列那道题很像。
这里简单看一下即可。
代码:
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
sort(envelopes.begin(),envelopes.end());// 排序
int n = envelopes.size();
vector<int> dp(n,1);
int ret = 1;
for(int i = 0; i < n; ++i)
{
for(int j = i - 1; j >= 0; --j)
{
if(envelopes[i][0] > envelopes[j][0] && envelopes[i][1] > envelopes[j][1])
dp[i] = max(dp[i],dp[j] + 1);
}
ret = max(ret,dp[i]);
}
return ret;
}
};
贪心 + 二分:
同样需要先排序,但是这里的排序就有点不一样了。
如果还是按照原来的方法排序(左端点从小到大),如果左端点是没有重复值的还好说,如上图的最上方的示例。但是如果遇见左端点有重复值时,极端的如上图 中间的示例,那么此时如果我们还是只看右端点做题的话, 4 6 7 9 这四个值都会被记录到结果中,然而这道题的题意是宽和高都要大于才行,等于是不行的。
面对这个问题,也不是不能解决,只是比较麻烦。有一个解决方法就很简单,将原来的排序规则改为: 左端点从小到大排序,如果左端点相同,那么就按照右端点从大到小排序。
这样 原来的 4 6 7 9 原本都会插入到结果数组中,但是现在变成了 9 7 6 4,那么此时只有4会插入到结果数组中。代码:
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& e) {
int n = e.size();
// 左端点按从小到大的顺序
// 左端点相同时,按右端点从大到小的顺序排序
sort(e.begin(),e.end(),[](const vector<int>& p1,const vector<int>& p2){
if(p1[0] < p2 [0]) return true;
else if(p1[0] == p2[0]) return p1[1] > p2[1];
else return false;
});
// 最长递增子序列的贪心解法流程
vector<int> tmp;
for(int i = 0; i < n; ++i)
{
// 特殊(边界)情况
if(tmp.empty() || e[i][1] > tmp.back())
{
tmp.push_back(e[i][1]);
}
else
{
// 通过二分查找来找到需要修改的位置
int left = 0,right = tmp.size() - 1;
while(left < right)
{
int mid = (right - left) / 2 + left;
if(e[i][1] > tmp[mid]) left = mid + 1;
else right = mid;
}
tmp[left] = e[i][1];
}
}
return tmp.size();
}
};
22.可被三整除的最大和

解法一: 正难则反 + 贪心 + 分类讨论
正难则反:我们可以直接先把所有的元素相加 ret,如果ret % 3 == 0,说明符合要求,直接返回。
否则就对 ret % 3 == 1 || ret % 3 == 2进行分类讨论。
在这里 用x1 表示数组中模3余数为1的最小值,x2 表示数组中模3余数为1的次小值。
那么同理 用 y1 表示余数为2的,y2表示次小值。

如上图,当 余数为1或者为2时,都有两种删法,以余数为1为例:
1.删掉 x1
2.删掉 y1 和y2
删掉其中的最小值即可。 那么当余数为2时分析同理。
另外关于找到最小值和次小值的问题上,我们可以用sort,但是它的时间复杂度是 O(N logN),我们通过分类讨论可以用 O(N)的时间复杂度。
代码:
class Solution {
public:
void func(int n,int& a,int& b)
{
if(n < a)
{
b = a;
a = n;
}
else if(n < b)
{
b = n;
}
}
int maxSumDivThree(vector<int>& nums) {
const int INF = 0x3f3f3f3f;
int ret = 0,x1 = INF,x2 = INF,y1 = INF,y2 = INF; // x表示余数为1,y表示余2
int n = nums.size();
for(int i = 0; i < n; ++i)
{
ret += nums[i];
if(nums[i] % 3 == 1)
{
func(nums[i],x1,x2); // 尝试更新x
}
else if(nums[i] % 3 == 2)
{
func(nums[i],y1,y2); // 尝试更新y
}
}
if(ret % 3 == 1)
{
ret -= min(x1,y1 + y2); // 取两种情况的最小值
}
else if(ret % 3 == 2)
{
ret -= min(y1,x1 + x2);
}
return ret;
}
};
解法二:动态规划
动态规划是解决这类问题的通用解法,比如 被四整除 被五整除 ... 而且当被整除的数大时,就不适合用上面的贪心解法了,因为if else 会写的特别多。
这里动态规划也不细讲了。
23.统计结果概率

解法:动态规划 || 暴力解法
暴力解法就是枚举所有的点数,时间复杂度为 O(6^n)
动态规划:
当有 n 个筛子时,此时的点数组合有 5 * n - 1 种。
状态表示:dp[i][j]表示 第 i 个筛子时,点数和为 j 的概率是多少
状态转移方程: dp[i][j] = dp[i - 1][j - k] (k = [1,6] ) + dp[i][j]。这里是逆向推导,会出现越界的情况,比如 dp[1][-3]...这种,我们可以用正向推导 dp[i][j + k] (k = [1,6] ) = dp[i][j + k] + dp[i - 1][j],这样写就不会出现越界问题。

填表顺序:从左往右。
本题需要返回的是一个数组,那么可以用一个一维的动态数组进行填表,并不断更新dp表的规模, 用循环来控制这是第几个筛子。
代码:
class Solution {
public:
vector<double> statisticsProbability(int num) {
vector<double> dp(6,1 / 6.0); // 第一个筛子时,所有组合概率都是 1/ 6
for(int i = 2; i <= num; ++i) // 从第二个筛子开始
{
vector<double> tmp(i * 5 + 1); // 下一个筛子的组合种数
for(int j = 0; j < dp.size(); ++j)
{
for(int k = 0; k < 6; ++k)
{
tmp[j + k] += dp[j] / 6.0; // 这样就不会有越界问题
}
}
dp = tmp; // 更新dp表规模
}
return dp;
}
};
24.破冰游戏

解法:暴力解法 || 数学解法
暴力解法 就是用一个链表把数据管理起来,然后每次模拟就找到对应的下标进行删除即可。时间复杂度为O(N ^ 2 ),超时。
数学解法:

这是一个正向推导的过程,num = 5,target = 3。也就是说每次向前移动3个位置。
最终的数组只剩下一个数了,下标也就是0。接着可以来反向推导:
第四轮反推,补上 m 个位置,然后模上当时的数组大小 2,位置是(0 + 3) % 2 = 1。
第三轮反推,补上 m 个位置,然后模上当时的数组大小 3,位置是(1 + 3) % 3 = 1。
第二轮反推,补上 m 个位置,然后模上当时的数组大小 4,位置是(1 + 3) % 4 = 0。
第一轮反推,补上 m 个位置,然后模上当时的数组大小 5,位置是(0 + 3) % 5 = 3。
所以最终剩下的数字的下标就是3。因为数组是从0开始的,所以最终的答案就是3。
总结一下反推的过程,就是 (当前index + m) % 上一轮剩余数字的个数。
代码:
class Solution {
public:
int iceBreakingGame(int num, int target) {
int index = 0;
for(int i = 0; i < num; ++i)
{
index = (index + target) % (i + 1);
}
return index;
}
};
25.从链表中删去总和值为零的连续节点

解法:前缀和 + 哈希
用一个sum统计当前所有结点的值相加的大小。
用一个哈希表记录sum的值所对应的节点。
共需要遍历两遍,第一次遍历将记录一个sum和所对应的节点,这里巧妙的是,如果sum值出现重复的了,说明中间的连续节点的值的和为零,这是一段需要删除的区间,而我们更新hash表时对于重复的sum值刚好是覆盖式的更新。

于是第一次遍历我们就把需要跳过区间的末尾节点给记录下来了。
接着第二次遍历

然后返回哨兵位的下一个节点就可以了。
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeZeroSumSublists(ListNode* head) {
unordered_map<int,ListNode*> hash;
int sum = 0;
ListNode* ret = new ListNode(0);
ret->next = head;
ListNode* cur = ret;
while(cur)
{
sum += cur->val;
hash[sum] = cur; // 对于重复出现的sum,会覆盖式的更新
cur = cur->next;
}
cur = ret;
sum = 0;
while(cur) // 更新结果
{
sum += cur->val;
// 注意这是hash[sum]->next,因为hash[sum]所代表的值是这个区间的末尾
cur->next = hash[sum]->next;
cur = cur->next;
}
return ret->next;
}
};
26.最长连续序列

这道题直接用排序就很简单,但是时间复杂度不符合题目中所要求的 O(N*logN)。
我这里使用的是哈希的思想。
大致流程:
先用hash set将数据里的元素统计出来,并进行去重。
然后再对hash表进行遍历时,这里的核心思路就是:
对于每一次遍历的curnum:
a. 如果 curnum - 1这个元素存在:那么说明它不是一个连续序列的开头,那么直接跳过。
b. 如果curnum - 1这个元素不存在:那么说明它是一段连续序列的开头,那么此时就可以用一个循环不断来判断下一个元素是否存在,如果下一个元素存在,那么该序列的长度就 + 1,直到遇到不存在的情况循环才会停止。
这就是这个算法的核心思路。每次记得统计序列的最大值即可。
并且这里的时间复杂度是O(N)的,虽然看似有两层循环,但一是我们已经将原数组中的元素去重了,二是仔细思考这个判断的过程,去重后的每个元素其实只会遍历两遍而已,所以时间复杂度就是 2N的,也就是 O(N)。
本题最关键的地方在于是否能想到 利用num - 1这样的策略 。
代码:
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> hash;
for(auto x : nums)
{
hash.insert(x); // 统计并进行去重
}
int ret = 0;
int len = 0;
for(auto& p : hash)
{
int curnum = p;
if(hash.count(curnum - 1) <= 0) // 如果curnum - 1不存在,说明它是一段连续序列的开头
{
len = 1;
// 比如题目给出的示例中,只有100 200 1符合条件 能够进入到这个循环里面
// 但是100 200 这里只会遍历一次,只有1会遍历4次,因此整体的时间复杂度还是O(N)
while(hash.count(curnum + 1) > 0)
{
++len;
++curnum;
}
ret = max(ret,len); // 记得更新结果
}
}
return ret;
}
};
27.盛水最多的容器


解法:双指针。
这道题要注意审题,我们需要求的是那个盛水最多的容器能盛多少水,而不是求一共能装多少水。
审题清楚后,我们需要先清楚怎么求一个容器的盛水容量,对于一个左临界点left,和一个右临界点right,它们所形成的容器所能盛水的容量就是:min(right的高度,left的高度) * (它们之间的距离)。
搞清楚怎么求盛水容量后,我们可以定义一个left = 0,right = n - 1(n是容器的大小)。用ret表示最终结果。
每次遍历,我们都会用刚刚求容量的方式来尝试更新ret,当left的高度比right的高度小时,那么就让left++,反之就让right++,直到left == right为止。
最终返回结果即可。
代码:
class Solution {
public:
int maxArea(vector<int>& height) {
int ret = 0;
int n = height.size();
int left = 0,right = n - 1;
while(left < right)
{
ret = max(ret,min(height[left],height[right]) * (right - left));
if(height[left] < height[right]) ++left;
else --right;
}
return ret;
}
};
28.三数之和


解法:双指针。
本题如果直接使用三层循环暴力解法的话时间复杂度为O(N ^ 3)。
我们可以先对数组进行一个排序,这样在选取三元组元素,假设元素分别是a b c, 从小到大排序完后,每次枚举时就能进行一个去重,比如能保证 a <= b <= c =》 {a,b,c},这样就不会再出现{b,c,a} 或者 {c,a,b}这样重复的三元组了。
我们用指针k来表示数组的开始,i,j分别表示k位置后面区间的开头和结尾。

假设tmp = nums[k] + nums[i] + nums[j]。
当tmp < 0时,++i
当tmp > 0 时, --j
当tmp == 0时,记录结果,然后 ++i ,--j。并且在这里需要再进行一步去重,如上有很多重复的-1,需要把这些-1进行去重,j同理。
每轮结束后记得++k,同样记得也需要对k进行去重。
另外去重的时候需要小心,会有一些特殊情况导致数组越界访问,比如{1,1,1}这种示例
代码:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> ret;
sort(nums.begin(),nums.end());
int k = 0, i = 1, j = n - 1;
while(k < n - 2)
{
i = k + 1;
j = n - 1;
while(i < j)
{
int tmp = nums[k] + nums[i] + nums[j];
if(tmp < 0)
{
++i;
}
else if(tmp > 0)
{
--j;
}
else
{
ret.push_back({nums[k],nums[i],nums[j]});
++i;
--j;
while(i < j && nums[i] == nums[i - 1]) ++i; // 去重,注意不要越界了
while(i < j && nums[j] == nums[j + 1]) --j;
}
}
++k;
while(k < n - 2 && nums[k] == nums[k - 1]) ++k;
}
return ret;
}
};
29.滑动窗口最大值

先看这道题的暴力解题思路:
首先需要遍历这个数组,那么时间复杂度就是O(N),每次遍历的时候,我们都需要找到当前滑动窗口的最大值,这个我们可以通过记录的方式以O(1)的时间复杂度完成,但是当我们移动滑动窗口的时候,left区间向右移动时需要删除元素,判断这个元素是否是最大值以及如果是最大值,删除后此时第二大的元素是什么,就需要遍历这个滑动窗口,遍历的时间复杂度是O(k),总结,如果使用暴力解法,那么时间复杂度就是O(N*K)。
然而存在一个优化的方法:

我们可以利用一个双端队列,每次right向右移动增加新元素时,如果这个元素比队列此时的尾部元素大,那么就对队列进行尾删,直到队列为空或者小于等于此时的队尾元素。这样的目的就是为了很方便的记录第二大的元素是谁。
记录结果和当left向右移动删除元素就很简单了。
代码:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq;
int n = nums.size();
int left = 0, right = 0;
vector<int> ret;
while (right < n || right - left == k)
{
if(right - left < k)
{
while(!dq.empty() && nums[right] > dq.back()) dq.pop_back();
dq.push_back(nums[right]);
++right;
}
else
{
ret.push_back(dq.front()); //记录结果
if(nums[left] == dq.front()) dq.pop_front();
++left;
}
}
return ret;
}
};
30.缺失的第一个正数
这道题难就难在我们需要通过时间复杂度O(N),空间复杂度O(1)来解决。
所以对数组进行排序,或者是用额外的容器(哈希表)都不符合题目要求。
解法:原地哈希的方式。
即我们直接将原数组视为一个哈希表。
我们把数组中的元素,1放在0号下标的位置,2就放在2号下标的位置,以此类推,直到每个元素都尽可能的被放在了它应该被放在的地方。以示例 3 4 -1 1为例:
结果:
我们发现,只有1号下标的元素不符合这个规则,于是我们就返回2即可。
关于放置元素位置存在两种特殊情况:
1.元素的规定位置不在这个数组区间内,比如小于等于0的数或者大于 数组大小的数。
2.出现重复值时,停止交换元素,比如数组 {1,1},当遍历到第二个1时,它会不断的跟第一个1进行交换,但是交换完之后,始终不符合规则,就会进入死循环。
当数组里的元素的位置尽可能的已经符合规则后,再遍历一遍数组,因为我们需要返回最小的正整数,所以只需要从小到大遍历,返回第一次出现不符合规则的元素的下标 再 + 1即可。
这里又存在一个特殊情况,如果数组中的元素都符合规则,比如 {1,2,3},那么此时返回数组的大小再 + 1即可。
代码:
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
for(int i = 0; i < n; ++i)
{
// 不断进行交换,直到这个位置符合规则
while(nums[i] != i + 1)
{
// 当出现特殊情况时,就及时退出循环,避免出现死循环
if(nums[i] <= 0 || nums[i] > n || nums[i] == nums[nums[i] - 1]) break;
swap(nums[i],nums[nums[i] - 1]);
}
}
for(int i = 0; i < n; ++i)
{
if(nums[i] != i + 1) return i + 1;
}
return n + 1; // 当数组内元素都符合规则时,返回数组大小 + 1
}
};
260

被折叠的 条评论
为什么被折叠?



