题目链接https://pintia.cn/problem-sets/994805342720868352/problems/994805439202443264
很久没有练习,为了考试开始临时抱佛脚。。
思维有点钝化,起初看到就是简单的遍历区间,但用脚趾头想都知道会超时。看了柳神思路大概是用二分法。但已经不记得二分法怎么写了,于是先抄了一遍,对着抄的代码再做分析。抄的时候感慨柳神写得真是漂亮,自己简直毫无改进余地。。
完整代码:
#include<iostream>
#include<vector>
#include<string>
#include<algorithm>
#include<stdio.h>
#include<math.h>
#include<map>
#include<set>
#include<queue>
#include<string.h>
using namespace std;
int N, M;
vector<int> s, ret;
void biSec(int i, int& j, int& tmpsum){
int left = i, right = N;
while (left < right){
int mid = (left + right) / 2;
if (s[mid] - s[i-1] >= M)
right = mid;
else
left = mid + 1;
}
j = right;
tmpsum = s[j] - s[i-1];
}
int main() {
cin>>N>>M;
s.resize(N+1, 0);
for (int i = 1; i <= N; i++){
cin>>s[i];
s[i] += s[i-1];
}
int minAns = s[N];
for (int i = 1; i <= N; i++){
int j, tmpsum;
biSec(i, j, tmpsum);
if (tmpsum > minAns)
continue;
if (tmpsum >= M){
if (tmpsum < minAns){
ret.clear();
minAns = tmpsum;
}
ret.push_back(i);
ret.push_back(j);
}
}
for (int i = 0; i < ret.size(); i+=2)
cout<<ret[i]<<"-"<<ret[i+1]<<endl;
return 0;
}
按照程序执行的思路,首先是先读取了数据,以前N位和的形式保存
for (int i = 1; i <= N; i++){
cin>>s[i];
s[i] += s[i-1];
}
这里的minAns
表示的应该是一串diamonds的子串“大于等于需求M”的最小的可能值,初始应为整串的总和,即s[N]
。
int minAns = s[N];
在下一个循环里做的事情是,得出一个合适的区间[i,j]
,在该区间内的和为tmpsum
。如果tmpsum
比最小可能和minAns
大,那么根本没必要考虑它(相当于Loss过大了);否则有可能要重置minAns
。
检查minAns
是否大于等于M
,因为这是必要条件。满足之后,如果tmpsum
比minAns
更小,说明找到Loss更小的值了,清空ret
数组,更新minAns
,再把区间push进结果数组ret
。而如果tmpsum == minAns
,即Loss不变,则不用更新,只需push。
for (int i = 1; i <= N; i++){
int j, tmpsum;
biSec(i, j, tmpsum);
if (tmpsum > minAns)
continue;
if (tmpsum >= M){
if (tmpsum < minAns){
ret.clear();
minAns = tmpsum;
}
ret.push_back(i);
ret.push_back(j);
}
}
下面来看关键的二分函数biSec
。二分的目的是,对每一个i
,找到一个合适的位置j
,使得i
到j
的区间和能够满足条件并且Loss最小。而这个j
是通过不断压缩left
和right
来得到的。
每次二分开始时,right
取最右的N
,left
至少要大于等于i
,所以从i
开始。
while
循环中不断缩减区间,candidate的j
实际上是从mid
来的,所以每次根据left
和right
计算出mid
后,只需要看i
到mid
的和是否大于等于M即可。
如果大了,说明区间右边太广,或者说重心偏右,要缩减右边界,因为已经满足大于等于M的条件,那么可以直接把右边界缩减为mid
。
如果小了,说明区间重心偏左,要增大左边界,让left++
就好。
通过这样一个过程,对每一个i
,都找到了最合适的j
以及它们的区间和tmpsum
,返还给主程序即可。
void biSec(int i, int& j, int& tmpsum){
int left = i, right = N;
while (left < right){
int mid = (left + right) / 2;
if (s[mid] - s[i-1] >= M)
right = mid;
else
left = mid + 1;
}
j = right;
tmpsum = s[j] - s[i-1];
}
注意到的一个小点:最终给j
赋值用的是right
,因为此时保证循环已经跳出来了(即left>=right
),那么这个right
就是从之前的mid
而来的,是正确的。由于left
是每次+1递增的,所以在跳出循环时最大可能是right+1
,这个是赋给j
是不正确的。
纸上得来终觉浅,就算抄了一遍代码,也是糊里糊涂的。只有把思路都写下来,相当于“讲给别人听”,才能更加深印象,理解透彻。