菜鸡今天又开始了一个新的算法,废话不多说,开始笔记。二分查找是一个十分常用的算法,适用于对带有单调性的数据进行处理或查找。大多数题目可能没那么显示,需要自己来找这个单调性在哪,所以这就是一个重点,具体的用法和代码很简单,就那么几行,但是对于折半的条件却需要有严格的要求。
[牛客]完全平方数
就如这个题目:多次查询[l,r]范围内的完全平方数个数。这样的题目通常需要经过预处理。因为数据量看起来比较大1e9,你也没法预处理哪个数是不是完全平方数。但是,完全平方数嘛,就必然是一个数的平方,那我们可以存一下,每一个数对应的完全平方数是谁。如:1->1,2->4,这样以此类推,正好我们的下标的平方就是我们存储的数据。而且我们也正好可以知道从1到这个数,已经有多少个完全平方数了,有点前缀和的感觉,这样,我们每遇到一个区间就可以直接二分查找两个端点的位置,然后做差,就正好是这个范围内所求性质的数的个数了。这里要注意一下,最好是要在二分查找的时候用左闭右开,这样你后面那个查到的永远是比你右端点大的位置,用这个直接减掉左端点就是其中数的个数了。
总结一下,我们的思想就是,大数据我们存不下,我们可以把某些具有该特性的数预处理先存在数组里面,借助数组下表当做前缀和,来相当于把一个大的区间进行了离散化,在进行二分查找。
#include
#include
#include
using namespace std;
long number[100010];
int main() {
for(int i = 1; i <= 100001; i++) number[i] = long(i)*i;
int n, l, r;
cin >> n;
while(n--) {
scanf("%d%d", &l, &r);
int lPos = lower_bound(number, number+100001, l)-number;
int rPos = upper_bound(number, number+100001, r)-number;
printf("%d\n", rPos-lPos);
}
return 0;
}
代码很简单的,对吧~
01分数规划的题目可以借助二分进行近似求解。至于什么是01分数规划,在这里稍微简单做下笔记,当然可以问度娘。
0/1分数规划模型是指,有一些二元组(si,pi),从中选取一些二元组,使得∑si / ∑pi最大(最小)。
这种题一类通用的解法就是,我们假设x = ∑si / ∑pi的最大(小)值,那么就有x * ∑pi = ∑si ,即∑si - x * ∑pi= 0。也就是说,当某一个值x满足上述式子的时候,它就是要求的值。所以我们直接二分答案,当上述式子>0,说明答案小了,<0则说明答案大了,这样计算即可。
[牛客]wyh的物品
#include
#include
#include
#include
using namespace std;
const int maxn = 1e5+5;
const double eps = 0.001;
double v[maxn], w[maxn], x[maxn];
int main() {
int T;
cin >> T;
while(T--) {
int n, k;
scanf("%d%d", &n, &k);
double l = 0, r = 0;
for(int i = 0; i < n; i++) {
scanf("%d%d", &w[i], &v[i]);
r = max(r, v[i]/w[i]);
}
double mid, sum = 0;
while(fabs(r-l) > eps) {
mid = (l+r)/2;
for(int i = 0; i < n; i++) x[i] = mid*w[i]-v[i];
//这里用我们假设的值求出来与原价值做差,然后排序,取最小的k个,
//如果和小于0,说明存在原来数据中的组合能够更优,需要往右区间找
//与上面的说法不太一样,但原理都是一样的,理解就好。这里直接使用
//sort函数就可以了,如果按照上述还需要写一个比较函数使排序从大到小
sort(x, x+n);
sum = 0.0;
for(int i = 0; i < k; i++) sum += x[i];
if(sum <= 0) l = mid;
else r = mid;
}
printf("%.2lf\n", mid);
}
return 0;
}
[牛客]andy的树被砍了
这种一般就比较简单吧,一般就是前缀和,毕竟是前缀和,也没有负数,单调性很明显,然后用二分的话可能代码就会简化一点,时间复杂度低一点。感觉可用可不用。我看好像都没用二分就过了。
#include
#include
#include
using namespace std;
const int maxn = 1e5+5;
long long c[maxn];
int h[maxn];
int main() {
int n;
cin >> n;
for(int i = 1; i <= n; i++) scanf("%d", &h[i]);
for(int i = 1; i <= n; i++) scanf("%lld", &c[i]), c[i] += c[i-1];
for(int i = 1; i <= n; i++) {
int pos = lower_bound(c+1, c+n+1, h[i]+c[i-1])-c;
printf("%d ", pos);
}
cout << endl;
return 0;
}
emmm,比暴力的代码更简洁一点。
有些题目要特别巧妙的使用前缀和,可以大量的减少时间复杂度:[牛客]transform,而且确定边界的时候一定要好好想想,要不然就一直错。
#include
#include
using namespace std;
const int maxn = 5e5+5;
int n;
long long T;
int x[maxn];
long long a[maxn], _sum[maxn], _cost[maxn];
inline long long getRightCost(int l, int r) {
return _cost[r]-_cost[l-1]-x[l]*(_sum[r]-_sum[l-1]);
}
inline long long getLeftCost(int l, int r) {
return (_sum[r]-_sum[l-1])*(x[r]-x[l])-getRightCost(l, r);
}
bool check(long long _try) {
int l, r, mid;
long long _half_try = (_try>>1)+(_try%2);
// @dev 先从左向右尝试,遍历左右边界,一点一点尝试
l = r = mid = 1;
while(true) {
while(r <= n && _sum[r]-_sum[l-1] < _try) r++;
if(r > n) break ; // @dev 如果r>n,说明全部取得都不够,直接返回
while(mid <= n && _sum[mid]-_sum[l-1] < _half_try) mid++;
long long _surplus = (_sum[r]-_sum[l-1]-_try)*(x[r]-x[mid]); //_sum[r]-_sum[l-1]-_try是整个区间全部取得多出来的,也会导致额外消耗,应该在后面减去
if(getLeftCost(l, mid)+getRightCost(mid, r)-_surplus <= T) return true;
l++;
}
l = r = mid = n;
while(true) {
while(l >= 1 && _sum[r]-_sum[l-1] < _try) l--;
if(l < 1) break ;
while(mid >= 1 && _sum[r]-_sum[mid-1] < _half_try) mid--;
long long _surplus = (_sum[r]-_sum[l-1]-_try)*(x[mid]-x[l]);
if(getLeftCost(l, mid)+getRightCost(mid, r)-_surplus <= T) return true;
r--;
}
return false;
}
int main() {
long long l = 0, r;
scanf("%d%lld", &n, &T);
T >>= 1;
for(int i = 1; i <= n; i++) scanf("%d", &x[i]);
for(int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
_sum[i] = _sum[i-1]+a[i];
_cost[i] = _cost[i-1]+x[i]*a[i];
l = max(a[i], l);
}
r = _sum[n];
while(l <= r) {
long long mid = (l+r)>>1;
if(check(mid)) l = mid+1;
else r = mid-1;
}
printf("%lld\n", r);
return 0;
}
一般我们习惯性的对所求答案进行二分,比如:[牛客]接机
一般的都是一个模板:
while(l < r) {
mid = (l+r)>>1;
if(check()) r = mid;
else l = mid+1;
}
重点在于根据题目怎么写这个check函数,还有就是对于边界的判断,要想好,哪一个是可行解,应该保留哪一个边界。
#include
#include
using namespace std;
const int maxn = 1e5+5;
int _time[maxn];
inline bool check(int n, int m, int c, int mid) {
// @dev 如果返回true,说明这个解可行
int pre = 0, cnt = 1;
for(int i = 1; i < n; i++) {
if(_time[i]-_time[pre] > mid || i-pre >= c) cnt++, pre = i;
}
return cnt <= m;
}
int main() {
int n, m, c;
scanf("%d%d%d", &n, &m, &c);
for(int i = 0; i < n; i++) scanf("%d", &_time[i]);
sort(_time, _time+n);
int mid, l = 0, r = _time[n-1]-_time[0]+1;
while(l < r) {
mid = (l+r)>>1;
if(check(n, m, c, mid)) r = mid;
//此解是可行的的,那么我们可以令r=mid,这样边界r就是可行的,但是l是否可行并不知道。
//这样,到最后循环结束的时候便是l==r,因为r一定是一个可行解,那么答案就是l或r。
else l = mid+1;
}
printf("%d\n", r);
return 0;
}
还有一种比较不容易出错的写法:
while(l <= r) {
mid = (l+r)>>1;
if(check(n, m, c, mid)) r = mid-1, _result = mid;
else l = mid+1;
}
这样把最后求得的可行解保存在一个变量里面,就可以不用边界是否正确,最后的解一定是可行的。
刚开始,没做多少题目,日后再更~