《算法竞赛进阶指南》读书笔记汇总
这里面是我在阅读《算法竞赛进阶指南》这本书时的一些思考,有兴趣可以瞧瞧!
如若发现什么问题,可以通过评论或者私信作者提出。希望各位大佬不吝赐教!
板子
整数域上的二分
在单调递增序列 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}
A1到Ai的和,则子段和可以转化为前缀和相减的形式,即:
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{sumi−min{sumj}},其中
i
−
j
≥
L
,
L
≤
i
≤
n
i - j \ge L, L \le i \le n
i−j≥L,L≤i≤n
仔细观察上面的式子可以发现,随着
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;
}