声明:

本系列博客是《算法竞赛进阶指南》+《算法竞赛入门经典》+《挑战程序设计竞赛》的学习笔记,主要是因为我三本都买了 按照《算法竞赛进阶指南》的目录顺序学习,包含书中的一些重要知识点、例题答案及我个人的学习心得和对该算法的补充拓展,仅用于学习交流和自己复习,无任何商业用途。博客中部分内容来源于书本和网络(我尽量减少书中引用),由我个人整理总结(习题和代码可都是我自己敲哒)部分内容由我个人编写而成 ,如果想要有更好的学习体验或者希望学习到更全面的知识,请于京东搜索购买正版图书:《算法竞赛进阶指南》— 作者李煜东,强烈安利,好书不火系列,谢谢配合。

下方链接为学习笔记目录链接(中转站)

学习笔记目录链接

ACM-ICPC在线模板

一、二分

0.二分法

二分是一个非常常见但是想要精通却很有讲究的一个垃圾 算法,二分的基础用法是在单调序列或单调函数中进行查找(二分查找),因此当问题的答案具有单调性,就可以通过二分把求解答案的问题转换成二分查找答案并判定答案的正确性(二分答案)。我们还可以用三分法去求解单峰函数的极值。

相信基础的二分大家都会,但是写出来二分不代表能A,因为二分的细节尤其重要,对于整数域上的二分,要注意终止边界和左右区间的取舍时的开闭情况,避免溜掉答案或造成死循环。对于实数域上的二分,要注意精度的问题。(我甚至在学习这一节之前,写的二分都是看运气,随缘调试,随缘AC,根本都没有注意到有这么多细节

1.整数域上的二分

本文中的二分保证最终答案处于闭区间 [ l , r ] [l,r] [l,r] 以内,并以 l = r l=r l=r 作为循环的结束条件

在单调递增序列a中查找>=x的数中最小的一个(即x或x的后继)


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

在单调递增序列a中查找<=x的数中最大的一个(即x或x的前驱)


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

作者给出的一套二分的流程:

  1. 分析问题,确定左右半段哪一个是可行区间,以及 m i d mid mid 归属哪一半段
  2. 根据分析结果,选择r=mid,l=mid+1,mid=(l+r)>>1l=mid,r=dmi-1,mid=(l+r+1)>>1
  3. 二分终止条件是l==r,该值就是答案所在的位置。

2.实数域上的二分

实数域二分,设置eps法

一般 e p s = 1 0 ( k + 2 ) eps=10^{-(k+2)} eps=10(k+2)(k是需要保留的位数)

while (l + eps < r) {
	double mid = (l + r) / 2;
	if (calc(mid)) r = mid; 
	else l = mid; 
}

实数域二分,规定循环次数法

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

二、三分

0.三分求单峰函数极值

有一类函数称为单峰函数,它们拥有唯一的极大值点,在极大值点左侧严格单调上升,在极大值点右侧严格单调下降;或者拥有唯一的极小值点,在极小值点左侧严格单调下降,在极小值点右侧严格单调上升。

为了避免混淆,我们称后一种函数为单谷函数,对于单峰或单谷函数,可以用三分求其极值。

以单峰函数 f ( ) f() f()为例,我们在函数的定义域 [ l , r ] [l,r] [l,r]上人去两个点 l m i d lmid lmid r m i d rmid rmid,其中 l < l i m d < r m i d < r l<limd<rmid<r l<limd<rmid<r,把函数分成三段:

如果 f ( l m i d ) < f ( r i m d ) f(lmid)<f(rimd) f(lmid)<f(rimd),则 l m i d lmid lmid r m i d rmid rmid要么同时处于极大值点左侧,要么分别位于极大值点两侧,无论是哪种情况,都可以确定极大值点在 l m i d lmid lmid右侧,可令 l = l m i d l=lmid l=lmid

如果 f ( l m i d ) > f ( r m i d ) f(lmid)>f(rmid) f(lmid)>f(rmid),则 l m i d lmid lmid r m i d rmid rmid要么同时处于极大值点右侧,要么分别位于极大值点两侧,无论是哪种情况,都可以确定极大值点在 r i m d rimd rimd左侧,可令 r = r m i d r=rmid r=rmid

如果在三分中遇到了 l m i d = r m i d lmid=rmid lmid=rmid,对于严格单调的函数,此时取 l = l m i d l=lmid l=lmid或者 r = r m i d r=rmid r=rmid 均可,对于非严格单调的函数,此时三分法不再适用。

1.P3382 【模板】三分法(秦九韶求多项式+三分)

#include<iostream>
#include<string.h>
#include<cstdio>
#include<math.h>
#include<map>
#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)
//#define int __int128
using namespace std;
typedef long long ll;//全用ll可能会MLE或者直接WA,全部换成int看会不会A,别动这里!!!
const ll N=110000;
const ll mod=1e9+7;
const double EPS=1e-10;//-10次方约等于趋近为0

ll n,m;
double a[20],l,r;

//神奇的秦九韶公式求多项式
//背!
inline double f(double x)
{
    double ans=0;
    lver(i,n,0){
        ans=ans*x+a[i];
    }
    return ans;
}
int main()
{
    cin>>n>>l>>r;
    lver(i,n,0)scanf("%lf",&a[i]);
    while(l+EPS<r){
        double x=(2*l+r)/3;//三分
        double y=(2*r+l)/3;
        if(f(x)>f(y))r=y;
        else l=x;
    }
    printf("%.5f\n",l);//输出要用f
    return 0;
}

三、二分答案转化为判定

一个宏观的最优化问题也可以抽象为函数,其定义域是该问题下的可行方案,对这些可行方案进行评估得到的数值构成函数的值域,最优解就是评估最优方案(不妨设评估越高越优)。假设最优评分是S,显然对于所有 > S >S >S的值,都不存在一个合法的方案达到此评分,否则就与S的最优性矛盾,而对于所有的 < S <S <S,一定存在一个合法方案达到或超过此评分。这样的问题的值域就具有一种特殊的单调性,在S的一侧合法,另一侧不合法,就像一个在 ( , S ] (-\infty, S] (,S]上为1,在 ( S , ) (S, \infty) (S,)上值为0的分段函数,可以通过二分找到这个分界点S。

借助二分,我们把求解最优解问题,转化为给定一个值 m i d mid mid,判断是否存在一个可行方案评分达到mid的问题。
看不太懂没关系,一看代码就明白了,主要理解思路

0.经典例子

有N本书排成一行,已知第i本的厚度是 A i A_i Ai 。把它们分成连续的M组,使T最小,其中T表示厚度之和最大的一组的厚度。

//把n本书分成m组,其中每一组的厚度之和都<=size,判断是否可行
bool valid(int size)
{
    int group = 1, reset = size;
    for (int i = 1; i <= n; ++i)
    {
        if (reset >= a[i])//如果放得下就继续放
        	reset -= a[i];
        //放不下就换一组新的
        else group++,reset = size - a[i]; 
    }
    return group <= m;
}
int main()
{
    int l = 0, r = sum;
    while (l < r)
    {
        int mid = (l + r) >> 2;
        if (valid(mid)) r = mid;
        else l = mid + 1;
    }
    cout << l << endl;
    return 0;
}

1.POJ 2018 Best Cow Fences( 最佳牛围栏 )(二分答案)

https://www.acwing.com/problem/content/104/

输入样例:

10 6
6 
4
2
10
3
8
5
9
4
1

输出样例:

6500

问题可以简化为一正整数数列,求一个平均数最大,长度不小于L的连续的子段。
那么就可以二分答案,把问题转化为判定“是否存在一个长度不小于L的子段,平均数不小于二分的值mid”。如果我们直接把数列的每一个数都减去二分的值,那么问题又变成了“是否存在一个长度不小于L的子段,子段和非负”。

(1)O(nlogn),二分+前缀和

比较简单的写法

#include<iostream>
#include<string.h>
#include<cstdio>
#include<math.h>
#include<map>
#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)
//#define int __int128
using namespace std;
typedef long long ll;//全用ll可能会MLE或者直接WA,全部换成int看会不会A,别动这里!!!
const ll N=100001;
const ll mod=1e9+7;
const double EPS=1e-5;//-10次方约等于趋近为0

double a[N],b[N],sum[N];
int n,L;
inline bool check(double x){
    over(i,1,n)
    b[i]=a[i]-x;
    over(i,1,n)
    sum[i]=sum[i-1]+b[i];
    double ans=-1e10;
    double min_val=1e10;
    //注意是长度不小于L,所以可以大于L,取不小于L中的最大值
    for(int i=L;i<=n;++i){
        min_val=min(min_val,sum[i-L]);
        ans=max(ans,sum[i]-min_val);
    }
    return ans>=0;
}

int main()
{
    cin>>n>>L;
    over(i,1,n)
        scanf("%lf",&a[i]);
    double l=-1e6,r=1e6;
    while(l+EPS<r){
        double mid=(l+r)/2;
        if(check(mid))l=mid;
        else r=mid;
    }
    cout<<int(r*1000)<<endl;
    return 0;
}

(2)优化!O(n),二分+单调队列

下面这道题跟本题基本类似,只是多了个右界的限制,删掉就好,然乎就一摸一样了。

【题解】P1419 寻找段落(二分+单调队列)难度⭐⭐⭐★
https://blog.csdn.net/weixin_45697774/article/details/104582688

2.AcWing 113. Innovative Business(特殊排序)

innovation n.创新,革新
原题链接:https://www.acwing.com/problem/content/description/115/

输入样例

[[0, 1, 0], [0, 0, 0], [1, 1, 0]]

输出样例

[3, 1, 2]

题解来源:https://www.acwing.com/solution/AcWing/content/1009/

// Forward declaration of compare API.
// bool compare(int a, int b);
// return bool means whether a is less than b.

class Solution {
public:
    vector<int> specialSort(int N) {
        vector<int>res;
        res.push_back(1);
        for(int i=2;i<=N;++i){
            int l=0,r=res.size()-1;
            while(l<=r){
                int mid=l+r>>1;
                if(compare(res[mid],i))l=mid+1;
                else r=mid-1;
            }
            res.push_back(i);
            for(int j=res.size()-2;j>r;j--)
            swap(res[j],res[j+1]);
        }
        return res;
    }
};

3.K-th Number (CCPC.2017)(二分+尺取)

链接:https://ac.nowcoder.com/acm/contest/19/B
来源:牛客网

题目描述

Alice are given an array A[1…N] with N numbers. Now Alice want to build an array B by a parameter K as following rules: Initially, the array B is empty. Consider each interval in array A. If the length of this interval is less than K, then ignore this interval. Otherwise, find the K-th largest number in this interval and add this number into array B. In fact Alice doesn’t care each element in the array B. She only wants to know the M-th largest element in the array B. Please help her to fi nd this number.
输入描述:
The first line is the number of test cases. For each test case, the first line contains three positive numbers N(1≤N≤105);K(1≤K≤N);M. The second line contains N numbers Ai(1≤Ai≤109).It’s guaranteed that M is not greater than the length of the array B.
输出描述:
For each test case, output a single line containing the M-th largest element in the array B.

示例1

输入

2
5 3 2
2 3 1 5 4
3 3 1
5 8 2

输出

3
2

题目大意:
给你数列A,对于A的每一个区间,求第K大,插入到数列B中,最后再求数列B的第M大
题目详解:

https://blog.csdn.net/codeswarrior/article/details/82827902

#include<iostream>
#include<string.h>
#include<cstdio>
#include<math.h>
#include<map>
#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)
//#define int __int128
using namespace std;
typedef long long ll;//全用ll可能会MLE或者直接WA,全部换成int看会不会A,别动这里!!!
const ll N=100001;
const ll mod=1e9+7;
const double EPS=1e-5;//-10次方约等于趋近为0

ll t,n,m,k;
ll a[N];

bool check(ll x){
    ll sum=0,s=0;
    for(int i=0,j=0;j<n;++j){
        if(a[j]>=x)sum++;
        if(sum==k){
            s+=n-j;
            while(a[i]<x){
                s+=n-j,i++;
            }
            sum--,i++;
        }
    }
    return s>=m;

}
int main()
{
    cin>>t;
    while(t--){
        cin>>n>>k>>m;
        over(i,0,n-1)scanf("%lld",&a[i]);
        ll l=1,r=1e9+10;
        while(l<r){
            ll mid=(l+r)>>1;
            if(check(mid))l=mid+1;
            else r=mid;
        }
        printf("%lld\n",l-1);
    }
    return 0;
}

注:如果您通过本文,有(qi)用(guai)的知识增加了,请您点个赞再离开,如果不嫌弃的话,点个关注再走吧,日更博主每天在线答疑 ! 当然,也非常欢迎您能在讨论区指出此文的不足处,作者会及时对文章加以修正 !如果有任何问题,欢迎评论,非常乐意为您解答!( •̀ ω •́ )✧