文章目录
- C++ 技术点
- 多边三角形剖分的最低得分(dp思路,选不选问题)
- 移动石子到连续(思路)
- 1027. 最长等差数列(动态规划)
- 1105. 填充书架(动态规划)
- 1031 两个非重叠子数组的最大和
- 1163.按字典序排在最后的子串(双指针,反证法)
- 1187 使数组严格递增(dp)
C++ 技术点
1. string类型使用find函数。
int index = s.find("@");
if (inde != string:npos){
xx
}
2. transform函数,将整个字符串做整体改变。
transform(s.begin(), s.end(), s.begin(), ::tolower);
多边三角形剖分的最低得分(dp思路,选不选问题)
- dp定义:dp[i][j]表示点 i 到点 j 的多边形的最小值。
- dp转移:转移方程的思路两种。选不选、选哪个。现在本题只有选哪个这个思路。因此
dp[i][j] = min(dp[i][j], dp[i,k]+dp[k,j]+cal(i,j,k))
。 - 枚举次序:求解dp[i][j]的时候是需要提前知道dp[i][k](k<j)这个更小的问题的,因此此时 j 是正序枚举;同样的对于dp[k][j]则是需要知道 k 才能知道 i 的,因此 i 是倒序枚举。
- dp初始值:在枚举次序分析以后,我们知道dp[i][i],dp[i][i+1] = 0, 其他的初始化为INT_MAX;
class Solution {
public:
int minScoreTriangulation(vector<int> &v) {
int n = v.size(), f[n][n];
memset(f, 0, sizeof(f));
for (int i = n - 3; i >= 0; --i)
for (int j = i + 2; j < n; ++j) {
f[i][j] = INT_MAX;
for (int k = i + 1; k < j; ++k)
f[i][j] = min(f[i][j], f[i][k] + f[k][j] + v[i] * v[j] * v[k]);
}
return f[0][n - 1];
}
};
移动石子到连续(思路)
比较有思维量的题目。首先我们需要思考下最终的结果是处于什么区间上的,也就是我们需要选在一个长度为 k 的区间最后将所有的石子搬运到这个区间上。
另外我们还需要思考,如何实现最大和最小的移动。
- 最大:以左右端点作为其中一个端点进行移动。这两个点之间的所有端点都是需要一次操作的。
- 最小:首先设计一个滑动窗,窗的两侧为实际存在的石子,并且左右两侧的石子的距离是小于 n 的最大距离。此时我们可以分情况讨论:
- 如果左右端点差为n,完全不需要动,返回0;
- 两个端点的距离差[stone[l], stone[r] ]恰好为n-1,石子数量也为n-1,也就是n-1个全部都是连续的。此时外面还剩一个,无论在什么位置,最多两次就可以连续。(认为右侧不动)
- 否则,填上中间的空挡即可。
class Solution {
public:
vector<int> numMovesStonesII(vector<int>& stones) {
int n = stones.size();
sort(stones.begin(), stones.end());
// max = max(stones[n-2] - stones[0] -1, stones[n-1]-stones[1]-1)
// min = 在一个滑动窗内 k个,因此需要n-k次
// 如果有n-1个是紧密相连的。那么 需要两次
if(stones[n-1]- stones[0]+1 == n){
return {0, 0};
}
int max_v = max(stones[n-2] - stones[0] +1, stones[n-1]-stones[1]+1)-(n-1);
int min_v = max_v;
int r = 1;
for (int l = 0;l<n;l++){
while(r < n && stones[r]-stones[l]+1 <= n){
r++;
}
r--;
// 左端点l到右端点r是连续的
if (r-l+1 == n-1 && stones[r]-stones[l]+1 == n-1){
min_v = min(min_v, 2);
}else{
min_v = min(min_v, n-(r-l+1));
}
}
return {min_v, max_v};
}
};
1027. 最长等差数列(动态规划)
最长子序列的问题很容易想到dp问题。选哪个问题
- dp定义:dp[i][j] 长度为 i 时,差值为 j 时的最大长度
- dp转移:dp[i][j] = max(dp[k][j]+(k,i)*maxValue, dp[i][j])
- dp初始值:0
- dp转移顺序:正序遍历 i ,反向遍历 k
class Solution {
public:
int longestArithSeqLength(vector<int> &a) {
int ans = 0, n = a.size(), f[n][1001];
memset(f, 0, sizeof(f));
for (int i = 1; i < n; ++i)
for (int j = i - 1; j >= 0; --j) {
int d = a[i] - a[j] + 500; // +500 防止出现负数
if (f[i][d] == 0) {
f[i][d] = f[j][d] + 1; // 默认的 1 在下面返回时加上
ans = max(ans, f[i][d]);
}
}
return ans + 1;
}
};
1105. 填充书架(动态规划)
选不选?选哪个? 选哪个问题
- dp定义:dp[i]表示前 i 本 书的最小高度。
- dp转移:dp[i] = min(dp[i], dp[j]+cal(j,i))。对于后 j 到 i 的书我们放在一个全新的书架上。
- dp状态:初始化为INT_MAX,但是dp[0] = 0;
class Solution {
public:
int minHeightShelves(vector<vector<int>>& books, int shelfWidth) {
// dp[i] 表示前i本书,总的高度
// dp[i] 选什么?当前层还是下一层? X
// 选哪个?从哪个开始放置在新的一层? ✅
int n = books.size();
vector<int> dp(n+1, INT_MAX);
dp[0] = 0; // 初始化为0
for (int i = 1;i<=n;i++){
int curmax = 0;
int curlength = 0;
for (int j = i;j>=1;j--){ // 选择某一个点,这个点之后都放在某一层
curlength += books[j-1][0];
// 超出了就停止,此时无法都放在一层
if(curlength > shelfWidth) break;
curmax = max(curmax, books[j-1][1]);
dp[i] = min(dp[i], dp[j-1]+curmax);
}
}
return dp[n];
}
};
1031 两个非重叠子数组的最大和
依然会有一点类似滑动窗口的思路,我们枚举second的起始位置 i ,可以表示firstLen的选取在[0,i]之间实现,second就是[i+1, i+secondLen]。 对于firsr 和 second可以交换前后的情况,我们可以分两种情况讨论即可。
class Solution {
public:
int help(vector<int>& nums, int firstLen, int secondLen) {
// dp[i] 表示前i个元素时,first数组的最大值,
// ans = dp[i]+sum(nums(i+1, i+secondLen))
int suml = accumulate(nums.begin(), nums.begin() + firstLen, 0);
int maxSumL = suml;
int sumr = accumulate(nums.begin() + firstLen, nums.begin() + firstLen + secondLen, 0);
int res = maxSumL + sumr;
// i表示second的结尾位置,j表示first的结尾位置
for (int i = firstLen + secondLen, j = firstLen; i < nums.size(); ++i, ++j) {
suml += nums[j] - nums[j - firstLen];
maxSumL = max(maxSumL, suml);
sumr += nums[i] - nums[i - secondLen];
res = max(res, maxSumL + sumr);
}
return res;
}
int maxSumTwoNoOverlap(vector<int>& nums, int firstLen, int secondLen) {
// 分两种情况分别讨论
return max(help(nums, firstLen, secondLen), help(nums, secondLen, firstLen));
}
};
1163.按字典序排在最后的子串(双指针,反证法)
这个题目的思路很独特,不知道更深层的思想是什么。首先有一个证明,答案一定是后缀字符串。只是我们要去选择这个开头的位置。因此我们可以比较不同的开头。
我们维护一个双指针,假设 i 是最佳的答案,我们尝试在 i 的后面寻找到一个 j 去证明 i 不是最好。
class Solution {
public:
string lastSubstring(string s) {
int n = s.size();
int i = 0;
int j = 1;
int k = 0;
// 假设 i 是最好的点,我们尝试找一个点j去推翻,如果找不到,就是最好的点。
// i表示当前最大的字串开始的位置,j是当前考虑的字串的位置,k是当前比较的位置
while( j + k < n) {
if (s[i + k] == s[j + k]) {
++k;
} else if (s[i + k] < s[j + k]) {
//因为i+k这个点不如j+k这个点,因此此时s[i+1,..,i+k]开头的的字串都不如s[j+1,..,j+k]开头的字串更好。都不考虑了,直接跳到新的点去论证。
i += k + 1;
k = 0;
// 如果i在j的位置以后了,我们更新j比较。前面的点都被推翻了。
if (i >= j) {
j = i + 1;
}
} else {
// s[i + k] > s[j + k] 我们需要找新的点去推翻,这个点就是j+k+1才可能
j += k + 1;
k = 0;
}
}
return s.substr(i);
}
};
1187 使数组严格递增(dp)
- dp[i][j] 表示对于前 i 个数字时候,替换了 j 次时的最后一个数字。这个定义比较奇特,没有直接定义所求内容。
- 转移方程:
- 如果arr[i] > dp[i-1][j] 那可以直接,dp[i][[j] = arr[i];
- 对于无法选择arr[i]或者不选择arr[i]的情况, 在arr2[j, end]里面寻找到第一个大于(upperbound)的 dp[i-1][j-1]的
- 初始:对于i= 0这个位置,dp[0][0] = arr[0], dp[0][1] = arr2[0], 其余的都初始化为int_MAX;
class Solution {
public:
int makeArrayIncreasing(vector<int>& arr1, vector<int>& arr2) {
// dp[i][j] 表示对于对于前i个arr1,替换j次时候的最小后缀;
// dp[i][j] = arr[i] 满足arr[i] > dp[i-1][j] 不进行替换
// dp[i][j] = min(dp[i][j], cal) 其中 cal是arr2中 index大于等于j,val严格大于dp[i-1][j-1]的最小值 j>0
// 初始化:dp[i][j] = INF dp[0][0] = arr[0] dp[0][1] = arr2[0]
sort(arr2.begin(), arr2.end());
arr2.erase(unique(arr2.begin(), arr2.end()), arr2.end());// 去重
int n = arr1.size();
int m = min((int)arr2.size(), n);
vector<vector<int>> dp(n, vector<int>(m+1, INT_MAX));
dp[0][0] = arr1[0];
dp[0][1] = arr2[0];
for(int i = 1;i<n;i++){
for(int j = 0;j<=min(i+1,m);j++){
if(arr1[i] > dp[i-1][j]) {
dp[i][j] = arr1[i];
}
if(j>0){
auto it = upper_bound(arr2.begin()+j-1, arr2.end(), dp[i-1][j-1]);
if(it != arr2.end()){
dp[i][j] = min(dp[i][j], *it);
}
}
if(dp[n-1][j] != INT_MAX) return j;
}
}
return -1;
}
};