0x04 内容简介与例题习题

《算法竞赛进阶指南》读书笔记汇总
这里面是我在阅读《算法竞赛进阶指南》这本书时的一些思考,有兴趣可以瞧瞧!
如若发现什么问题,可以通过评论或者私信作者提出。希望各位大佬不吝赐教!

板子

整数域上的二分

在单调递增序列 a a a中查找 > = x >= x >=x的数中最小的一个(也就是STL的lower_bound)

while(l < r){
	int mid = l + r >> 1;
	if(a[mid] >= x) r = mid;
	else l = mid + 1;
}
return a[l];

在单调递增序列 a a a中查找 < = x <=x <=x的数中最大的一个

while(l < r){
	int mid = l + r + 1 >> 1;
	if(a[mid] <= x) l = mid;
	else r = mid - 1;
}
return a[l];

实数域上的二分

在保留 k k k位小数时,取 e p s = 1 0 − ( k + 2 ) eps = 10^{-(k +2)} eps=10(k+2)

while(r - l > 1e-5){
	double mid = (l + r) / 2;
	if(check(mid)) r = mid;
	else l = mid;
}

或指定迭代次数

for(int i = 0;i < 100;i ++){
	double mid = (l + r) / 2;
	if(check(mid)) r = mid;
	else l = mid;
}

三分求单峰函数极值

求单峰函数 f ( x ) f(x) f(x)的极大值

while(r - l > eps){
	double lmid = l + (r - l) / 3;
	double rmid = l + (r - l) / 3 * 2;
	if(f(lmid) < f(rmid)) l = lmid;
	else r = rmid;
}
return f(l);

二分答案

一般看到使最小值最大或者最大值最小就可以联想二分答案。
我们把求最优解的问题,转化为给定一个值 m i d mid mid,判定是否存在一个可行方案评分达到 m i d mid mid的问题。求解这类问题的关键在于,找到题目中隐藏的单调性。

【例题】最佳牛围栏(AcWing102)

题目链接
思路: 二分出一个 m i d mid mid,判定“是否存在一个长度为 L L L的子段,平均数不小于 m i d mid mid
那么如果把数列中的每一个数都减去 m i d mid mid,那么问题转化为判定“是否存在一个长度为 L L L的子段,子段和大于等于零”。那么其实就是求长度不小于 L L L的最大子段和,看这个和是否非负即可。
如果忽略长度限制,那么这个问题是经典的最大子段和问题。可用递推来求解(具体参考这道题
如果我们考虑长度限制,设 s u m i sum_{i} sumi表示 A 1 到 A i A_{1}到A_{i} A1Ai的和,则子段和可以转化为前缀和相减的形式,即: m a x { A j + 1 + . . . + A i } = m a x { s u m i − m i n { s u m j } } max\{A_{j + 1}+...+A_{i}\} = max\{sum_{i} - min\{sum_{j}\}\} max{Aj+1+...+Ai}=max{sumimin{sumj}},其中 i − j ≥ L , L ≤ i ≤ n i - j \ge L, L \le i \le n ijL,Lin
仔细观察上面的式子可以发现,随着 i i i的增长, j j j每次只会增加1,那么我们可以用一个变量记录当前最小的 s u m j sum_{j} sumj即可,没必要循环 j j j了。
这样我们可以求出长度不小于 L L L的最大的子段和,如果这个子段和非负,那么说明当前的 m i d mid mid满足要求可以继续变大;否则 m i d mid mid必须变小。(想想为什么)

AC代码:

#include<bits/stdc++.h>
#define LL long long
#define N 100005
using namespace std;

int n,m;
double a[N],b[N],sum[N];

void solve(){
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i ++)
        scanf("%lf",&a[i]);
    double l = -1e9,r = 1e9;
    while(r - l > 1e-6){
        double mid = (l + r) / 2;
        for(int i = 1;i <= n;i ++)
            b[i] = a[i] - mid,sum[i] = sum[i - 1] + b[i];
        double ans = -1e18,mi = 1e18;
        for(int i = m;i <= n;i ++){
            mi = min(sum[i - m],mi);
            ans = max(ans,sum[i] - mi);
        }
        if(ans >= 0) l = mid;
        else r = mid;
    }
    printf("%d\n",(int)(r * 1000));
}

int main(){
    solve();
    return 0;
}

习题

【练习】赶牛入圈(AcWing121)

题目链接
思路: 首先我们肯定需要二分正方形的长度,假设当前二分的长度为 l e n len len,我们来考虑一下怎么判定。
不难想到我们可以枚举所有正方形,判断每一个正方形里草的数目是否满足条件即可。枚举正方形只需要枚举左上角,右下角可以通过长度和左上角计算出来,但是这题的坐标范围为 [ 1 , 10000 ] [1,10000] [1,10000],这样做肯定会超时。
我们思考如何减少枚举数目,考虑一个最优性问题,就是考虑最优解有什么性质。通过观察我们发现这样一个性质,最优解正方形右下角的两条临边上必定有点。
可以通过画图证明这个性质,如下图:
在这里插入图片描述

我们可以通过如下变换使得右下角的两条临边上有点,且解不会变差。
首先把正方形缩小使得至少有两条对边上有点
在这里插入图片描述
然后通过移动使得下边或者右边有点
在这里插入图片描述
有了这个性质之后,我们枚举的右下角坐标范围就在所有草的坐标范围内,因此我们可以把所有草的坐标进行离散化,预处理出前缀和,通过双指针进行枚举即可。
为什么要用双指针,我直接枚举右下角不就行吗?但是你的左上角下标可能不在草的坐标里啊,这样怎么用前缀和算嘛。
这题还有一个细节,就是草并不是一个点,是一个区域,写代码的时候要注意一下(好吧其实实际上写起来和一般的前缀和也没有什么差别)

AC代码:

#include<bits/stdc++.h>
#define N 1010
#define INF 0x3f3f3f3f
#define PII pair<int,int>
using namespace std;

int c,n;
int b[N];
PII p[N];
int a[N][N];
int cnt,len;

int get(int x){
    return lower_bound(b + 1,b + 1 + len,x) - b;
}

bool check(int x){
    for(int x1 = 1,x2 = 1;x2 <= len;x2 ++){
        while(b[x2] - b[x1] + 1 > x) x1 ++;
        for(int y1 = 0,y2 = 1;y2 <= len;y2 ++){
            while(b[y2] - b[y1] + 1 > x) y1 ++;
            if(a[x2][y2] - a[x1 - 1][y2] - a[x2][y1 - 1] + a[x1 - 1][y1 - 1] >= c) return true;
        }
    }
    return false;
}

void solve(){
    scanf("%d%d",&c,&n);
    for(int i = 1;i <= n;i ++){
        int x,y;
        scanf("%d%d",&x,&y);
        p[i] = {x,y};
        b[++ cnt] = x,b[++ cnt] = y;
    }

    sort(b + 1,b + 1 + cnt);
    len = unique(b + 1,b + 1 + cnt) - b - 1;

    for(int i = 1;i <= n;i ++){
        int x = get(p[i].first),y = get(p[i].second);
        a[x][y] ++;
    }

    for(int i = 1;i <= len;i ++)
        for(int j = 1;j <= len;j ++)
            a[i][j] += a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];

    int l = 1,r = 10000;
    while(l < r){
        int mid = (l + r) >> 1;
        if(check(mid)) r = mid;
        else l = mid + 1;
    }

    printf("%d\n",l);
}

int main(){
    solve();
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值