「二分与三分」课件配套资料

写在前面

讲课课件配套资料。
课件下载地址:
链接:https://pan.baidu.com/s/1hiFYRS3MeE0Yms7r4cWTFA
提取码:fmj1
隐去了部分涉及个人隐私的部分。


概念+典例篇

二分法

指的是将一个整体事物分割成两部分。
也即是说,这两部分必须是互补事件,即所有事物必须属于双方中的一方,且互斥,即没有事物可以同时属于双方。
在 OI 中的应用:二分查找,二分答案。


二分查找

用来在一个 有序数组 中查找某一元素是否出现的算法。

以在一个升序数组中查找一个数为例,每次考察数组当前部分的中间元素。

  1. 若中间元素刚好是要找的,就结束搜索过程。
  2. 若中间元素小于所查找的值,那么左侧的只会更小,不会有所查找的元素,只需要到右侧去找就好了。
  3. 若中间元素大于所查找的值,同理,右侧的只会更大而不会有所查找的元素,所以只需要到左侧去找。

搜索过程中,每次都把查询的区间减半。
因此对于一个长度为 \(n\) 的数组,至多会进行 \(𝑂(\log_2⁡ n)\) 次查找。

int a[kMaxn];
int binary_search(int l_, int r_, int key_) {
  int l = l_, r = r_, ret = -1;
  while (l <= r) {
    int mid = l + ((r - l) >> 1); //防溢出
    if (a[mid] < key_) {
      l = mid + 1;
    } else if (a[mid] > key_) {
      r = mid - 1;
    } else {
      ret = mid;
      break;
    }
  }
  return ret;
}

二分查找是分治吗

二分查找是一种比较特别的分治,它满足分治的三个关键字:

  1. 容易求解的边界:区间左右端点收敛。
  2. 相互独立的子问题:对一半进行查找不会影响另一半。
  3. 可合并的子问题:区间答案为合法的一边的答案。

但在分解问题这一步中,它利用了数组的单调性,直接将两个子问题中不合法的一个Pass掉,从而减小了问题规模

这种缩小问题规模的分治被称为 减治法


小应用

查找单调不降数组中大于某个数的第一个位置。
比较好理解,只要出现大于查询值的位置就赋值,并收敛右区间。
返回值一定是大于查询值的第一个位置。

int a[kMaxn];
int binary_search(int l_, int r_, int key_) {
  int l = l_, r = r_, ret = -1;
  while (l <= r) {
    int mid = l + ((r - l) >> 1); //防溢出
    if (a[mid] > key_) {
      ret = mid;
      r = mid - 1;
    } else {
      l = mid + 1;
    }
  }
  return ret;
}

抽象模型

考虑上一个例子。

在给定查询值后,由于数列具有单调性,可将查询区间中小于等于查询值的数全赋成 0,将大于查询指的数全赋成 1。问题变为查询第一个 1 的位置。

换言之,二分搜索可用来查找满足某种条件的最大(最小)的值。


STL的二分查找

对于一个有序的数组:

可使用函数 lower_bound() 来找到第一个大于等于给定值的数,使用 upper_bound() 来找到第一个大于给定值的数。
这两个函数在 中被定义。

请注意,必须是有序数组,否则答案是错误的。
下面这个程序的输出是 2 4。

int a[kMaxn];
int main() { 
  a[1] = 1; a[2] = 2; a[3] = 2; a[4] = 5; a[5] = 7;
  printf("%d\n", lower_bound(a + 1, a + 5 + 1, 2) - a);
  printf("%d\n", upper_bound(a + 1, a + 5 + 1, 2) - a);
  return 0; 
}

可能会有的一些疑问

括号里为啥要这样写啊?
括号里+1是为了干啥啊?
a 不是数组名吗为什么还能减的啊?
而且最后为啥要减掉 a 啊?

可以类比 sort 的使用,建议多用几遍体会一下后就会了。
涉及到函数返回值为地址,数组是指针变量之类的问题,在算法竞赛中没有必要深究,想搞懂可以来单独找我要 《C++ Primer》阅读一下。


二分答案

通过一道例题引入。

有一排高度不同的树,ZlycerQan有一个伐木机。
他可以任意设定锯片的高度,将n棵树比锯片高度高的部分砍下来。

例:树高分别为 20,15,10,17,若锯片高度为 15,切割后树剩下的高度为 15,15,10,15。他可以从第一棵树得到 5 的木材,从第四棵树得到 2 的木材。

给定所有树的高度,至少需要的木材长度,求最高的锯片高度。

洛谷P1873 砍树

发现锯片越高,得到的木材越少。
直观想法是直接从小到大枚举锯片高度,并检查是否合法,最后一个合法的高度即为答案。
然后可以写出这样的代码,并获得了 40pts 的好成绩。

//知识点:暴力枚举
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e6 + 10;
//=============================================================
int n, m, maxa, a[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Getmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Getmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
bool Check(int h_) {
  ll sum = 0;
  for (int i = 1; i <= n; ++ i) {
    if (a[i] > h_) sum += 1ll * (a[i] - h_);
  }
  return sum >= 1ll * m;
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) {
    a[i] = read();
    Getmax(maxa, a[i]);
  }
  int ans = 0;
  for (int i = 1; i <= maxa; ++ i) {
    if (! Check(i)) break;
    ans = i;
  }
  printf("%d\n", ans);
  return 0;
}

复杂度瓶颈在枚举答案上。

发现枚举的锯片高度的取值存在一个分界线,使分界线左侧所有高度均能获得足够的木材,右侧所有高度均不能获得足够的木材。
答案即分界线左侧的第一个数。

考虑二分查找单调不降数组中大于某个数的第一个位置时抽象出的模型:二分搜索可用来查找满足某种条件的最大(最小)的值。
此题中满足某种条件即为能获得足够的木材,答案即满足条件的最大值。

可以很方便地检查一个答案是否合法,考虑用二分,对枚举答案的过程进行优化。
然后可以写出这样的代码。
一共二分 \(𝑂(\log_2⁡ (\max a))\)次,每次二分 \(O(n)\) 地检查一个答案,总复杂度 \(𝑂(n\log_2⁡ (\max a))\)

正解:

//知识点:二分答案
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e6 + 10;
//=============================================================
int n, m, maxa, a[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Getmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Getmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
bool Check(int h_) {
  ll sum = 0;
  for (int i = 1; i <= n; ++ i) {
    if (a[i] > h_) sum += 1ll * (a[i] - h_);
  }
  return sum >= 1ll * m;
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) {
    a[i] = read();
    Getmax(maxa, a[i]);
  }
  int l = 1, r = maxa, ans;
  while (l <= r) {
    int mid = (l + r) >> 1;
    if (Check(mid)) {
      ans = mid;
      l = mid + 1;
    } else {
      r = mid - 1;
    }
  }
  printf("%d\n", ans);
  return 0;
}

小结

在答案的可能范围内二分,并检查所枚举举的答案是否符合条件。
利用答案的 单调性 进行减治。
可将直接求解较难的最优性问题,转化为判定相对容易的可行性问题。

当且仅当答案范围已知,且具有单调性时才可使用。
一般可用于解决「最小值最大」和「最大值最小」的最优化问题,可在之后的例题中体会。


似乎是一个板子

一般是这样的套路,我至今没有遇到其他的写法。
注意二分里边界的改变要就题论题。

网上许多写法将 \(l,r\) 作为答案输出,让人产生二分的边界很诡异的感觉。
我认为这是野蛮的,正确的写法是将最后一个检查到的合法的,设成答案。

int l = 1, r = maxa, ans;
while (l <= r) {
  int mid = (l + r) >> 1;
  if (Check(mid)) {
    ans = mid;
    l = mid + 1;
  } else {
    r = mid - 1;
  }
}
printf("%d\n", ans);
return 0;

扯几句

没有出题人单独考二分查找和二分答案,它们只是出题时一点与其他知识结合的小 Trick。
需要深入理解原理,因为结合的知识可以不那么简单。

个人遇到过的,看看就行,现在没有必要会:
「NOIP2018」赛道修建:二分答案 套 二分答案 + 不是很难的贪心。
「CTSC2018」混合果汁:二分答案 + 主席树上二分。
「TJOI / HEOI2016」字符串:二分答案 套 后缀自动机+线段树合并 or 后缀数组+st表+主席树二维数点。


三分法

一种基于减治的,求单峰函数区间内极值的算法。
每次取区间的两个三等分点,作为自变量计算出它们对应函数值。
舍弃函数值较小一边对应的 1/3 的定义域。
分类讨论证明正确性。
上黑板画图。

模板题

给定一个 \(n\) 次函数和定义域 \([l,r]\)
保证在范围 \([l,r]\) 内存在一点 \(x\),使得 \([l, x]\) 上单调增,\([x, r]\) 上单调减。试求出 \(x\) 的值。
输出时四舍五入保留 \(5\) 位小数。

例子:\(f(x) = x^3−3x^2−3x+1\),区间为 \([−0.9981, 0.5]\)
\(x = −0.41421\) 时图像位于最高点,故此时函数在 \([l, x]\) 上单调增,\([x, r]\) 上单调减,故 \(x = −0.41421\),输出 \(−0.41421\)

代码

洛谷 P3382 【模板】三分法

实数域上的边界不能简单比较是否相等,设置了一个近似于0的常量 eps 来确定边界。

注意三等分点的计算方法:
左侧的三等分点 \(=l+\dfrac{(r−l)}{3}=\dfrac{(2l+r)}{3}\)
右侧的三等分点 \(=l+\dfrac{2(r−l)}{3}=\dfrac{(l+2r)}{3}\)

另外该多项式点值的计算使用了秦九韶算法。
比较好理解,上黑板口胡。

//知识点:三分法
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 15;
const double eps = 1e-6;
//=============================================================
int n;
double L, R, a[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void GetMax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void GetMin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
double f(double x_) {
  double ret = 0;
  for (int i = 1; i <= n + 1; ++ i) {
    ret = x_ * ret + a[i];
  }
  return ret;
}
//=============================================================
int main() { 
  n = read();
  scanf("%lf %lf", &L, &R);
  for (int i = 1; i <= n + 1; ++ i) scanf("%lf", &a[i]);

  double l = L, r = R;
  while (fabs(r - l) >= eps) {
    double mid1 = (2.0 * l + r) / 3.0, mid2 = (l + 2.0 * r) / 3.0;
    if (f(mid1) > f(mid2)) {
      r = mid2;
    } else {
      l = mid1;
    }
  }
  printf("%.5lf", l);
  return 0; 
}

例题篇

板子题

P2249 【深基13.例1】查找

二分查找板子。

//知识点:二分查找
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e6 + 10;
//=============================================================
int n, m, a[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Getmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Getmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
int BinarySearch(int l_, int r_, int key_) {
  int l = l_, r = r_, ret = 0;
  while (l <= r) {
    int mid = l + ((r - l) >> 1);
    if (a[mid] >= key_) {
      ret = mid;
      r = mid - 1;
    } else {
      l = mid + 1;
    }
  }
  if (a[ret] == key_) return ret;
  return -1;
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) a[i] = read();
  for (int i = 1; i <= m; ++ i) {
    int key = read();
    printf("%d ", BinarySearch(1, n, key));
  }
  return 0;
}

大水题

洛谷P1824 进击的奶牛

二分答案+贪心。
最大化最近距离。最近距离越小,放置的限制越少,就越有可能放开所有奶牛,答案存在单调性。

二分枚举一个最近距离,问题变为放下 \(c\) 头奶牛后,所有相邻两头奶牛的最近距离≥ 枚举量是否合法。

枚举所有牛棚坐标,贪心地放置奶牛。先在第一个牛棚放置一头奶牛,并记录上一头奶牛放置的位置。
按坐标升序枚举牛棚,若当前牛棚与上一头奶牛距离超过枚举量就放置,否则不放置。
判断最后能放置的奶牛数是否大于 \(c\) 即可。

//知识点:二分答案,贪心
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e5 + 10;
//=============================================================
int n, c, maxa, a[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Getmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Getmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
bool Check(int lim_) {
  int cnt = 1, last = a[1];
  for (int i = 2; i <= n; ++ i) {
    if (a[i] - last >= lim_) {
      cnt ++;
      last = a[i];
    }
  }
  return cnt >= c;
}
//=============================================================
int main() {
  n = read(), c = read();
  for (int i = 1; i <= n; ++ i) a[i] = read();
  std :: sort(a + 1, a + n + 1);
  maxa = a[n];
  int l = 1, r = maxa, ans;
  while (l <= r) {
    int mid = (l + r) >> 1;
    if (Check(mid)) {
      ans = mid;
      l = mid + 1;
    } else {
      r = mid - 1;
    }
  }
  printf("%d\n", ans);
  return 0;
}

很简单题

P2658 汽车拉力比赛

以下定义从一个格出发,可到达与之相邻的四个格,其难度为两相邻单元格内整数的差的绝对值。

最小化最大难度。难度系数越大,能走过的相邻格越多,就越有可能令所有关键格连通,满足单调性。

二分一个难度系数,数据范围不大,考虑广度优先搜索进行判断。
以任意一个关键格为起点开始搜索,转移时只能转移到难度≤枚举值的相邻格。
如果转移到了一个关键格,打标记,最后判断所有关键格是否都有标记。

也可以把格子看成点,相邻格的转移看成一条边,问题变为只经过边权 ≤ 枚举值的边是否与所有关键点连通,并查集直接做。

搜索做法,来自 洛谷用户 @windows_10

# include <bits/stdc++.h> // 头文件 
using namespace std; // 名字空间 
const int N=505; bool c[N][N], sign=1, vis[N][N];
// 数组范围,路标,第一个路标,是否访问 
int n, m, a[N][N], l, r, mid, ans, st, en, tp;
// n,m,高度,左(二分),右(二分),中(二分),答案,
// 开始路标的x坐标,答案,开始路标的y坐标,路标个数 
int dx[5]={0, 0, 1, 0, -1},
	dy[5]={0, 1, 0, -1, 0};
// 方向增量数组 
bool bfs () { // 伟大的bfs 
	queue <int> x, y; // 定义队列,x坐标,y坐标 
	int now=1; // 记录已访问的路标,之前用了队列z,错了2个小时,有毒 
	x.push (st), y.push (en); // 初始压入队列 
	vis[st][en]=1; // 起点为已访问 
	while (!x.empty ()) { // 判断结束 
		int xx=x.front (), yy=y.front (); // 记录x和y 
		if (now==tp) return 1; // 如果已经访问了所有的路标,可以退出 
		x.pop (), y.pop (); // 出队 
		for (int i=1; i<=4; i++) { // 开始像四个方向搜索 
			int nx=xx+dx[i], ny=yy+dy[i]; // 计算现在的坐标 
			if (nx<1 || nx>n || ny<1 || ny>m || vis[nx][ny]) continue;
			// 如果越界或访问过,不再执行入队操作 
			int tpp=abs (a[nx][ny]-a[xx][yy]); // 计算高度差,要绝对值 
			if (tpp>mid) continue;
			// 高度差大于期望值(mid),不符合要求,不再执行入队操作 
			else { 
				if (c[nx][ny]) now++; // 如果是路标,访问数加一 
				x.push (nx), y.push (ny); // 入队 
				vis[nx][ny]=1; // 标记为已访问 
			} 
		}
	}
	return 0;
}
int main () {
	freopen ("node.in", "r", stdin);
	freopen ("node.out", "w", stdout);
	scanf ("%d%d", &n, &m); // 读入 
	for (int i=1; i<=n; i++) // 读入 
		for (int j=1; j<=m; j++) { // 读入 
			scanf ("%d", &a[i][j]); // 读入 
			r=max (r, a[i][j]); // 右端点为高度最大值 
		} // 读入 
	for (int i=1; i<=n; i++) // 读入 
		for (int j=1; j<=m; j++) { // 读入 
			scanf ("%d", &c[i][j]); // 读入 
			if (c[i][j]) tp++; // 如果是路标,记录个数(加一) 
			if (sign && c[i][j]) { // 如果是第一个路标 
				st=i; en=j; sign=0; // 记录坐标,取消第一个标记 
			}
		} 
	while (l<=r) { // 二分开始 
		mid=(l+r)/2; // 计算中间值 
		memset (vis, 0, sizeof vis); // 清空访问数组 
		if (bfs ()) // 如果这个值可以访问到所有路标 
			r=mid-1, ans=mid; // 记录答案,缩小右端点 
		else l=mid+1; // 如果这个值不可以访问到所有路标,扩大左端点 
	} 
	printf ("%d\n", ans); // 输出答案 
	return 0;
}

并查集做法:

//知识点:二分答案,并查集
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 510;
//=============================================================
int n, m, maxans, h[kMaxn][kMaxn];
int flagnum, flag[kMaxn * kMaxn];
int fa[kMaxn * kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Getmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Getmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
int Id(int x_, int y_) {
  return (x_ - 1) * m + y_;
}
void Prepare() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= m; ++ j) {
      h[i][j] = read();
      Getmax(maxans, h[i][j]);
    }
  }
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= m; ++ j) {
      int now = read();
      if (now) flag[++ flagnum] = Id(i, j);
    }
  }
}
int Find(int u_) {
  return fa[u_] == u_ ? u_ : fa[u_] = Find(fa[u_]);
}
void Unite(int u_, int v_) {
  u_ = Find(u_), v_ = Find(v_);
  if (u_ == v_) return ;
  fa[u_] = v_;
}
bool Check(int lim_) {
  for (int i = 1; i <= Id(n, m); ++ i) {
    fa[i] = i;
  }
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= m; ++ j) {
      if (i < n) {
        if (abs(h[i][j] - h[i + 1][j]) <= lim_) {
          Unite(Id(i, j), Id(i + 1, j));
        }
      }
      if (j < m) {
        if (abs(h[i][j] - h[i][j + 1]) <= lim_) {
          Unite(Id(i, j), Id(i, j + 1));
        }
      }
    }
  }

  for (int i = 1; i < flagnum; ++ i) {
      if (Find(flag[i]) != Find(flag[i + 1])) return false;
  }
  return true;
}
//=============================================================
int main() {
  Prepare();
  int l = 0, r = maxans, ans;
  while (l <= r) {
    int mid = (l + r) >> 1;
    if (Check(mid)) {
      ans = mid;
      r = mid - 1;
    } else {
      l = mid + 1;
    }
  }
  printf("%d\n", ans);
  return 0;
}

⑨都会的题

P1462 通往奥格瑞玛的道路

最小化路径最大点权。最大点权越大,允许到达的点就越多,就越有可能找到一条满足边权值之和\(<\)给定参数 \(b\) 的路径。答案满足单调性。

二分最大点权,判断只经过点权<枚举量的点,从 \(1\)\(n\) 的最短路是否\(<\)给定参数 \(b\)
没有负权,随便写个最短路就行。

懒得重写了,贴一份之前写的代码。
顺带一提,这个代码里 spfa 优先队列优化能跑过标准版的单元最短路。
但还是很假,该被卡还是会被卡。

// luogu-judger-enable-o2
//算法:二分答案/图论
/*
题目要求 经过的所有城市中 最多的 一次收取的费用的 最小值 是多少
根据题目, 显然,答案满足单调性,
即较小答案合法,则比他大的答案都合法
就可以使用二分答案法

二分答案枚举 最多的一次收取的费用的最小值
并将其作为一个参数 x ,传递给子函数
在子函数中,跑一遍SPFA,
以损失的血量尽量少为目的,
同时对于收取费用大于x的城市,不可到达.

跑完一遍后,判断到达n点消耗的血量是否小于最大血量
如果比最大血量大,证明最多的一次收取的费用的最小值为x时,不可到达
否则则可到达

二分得到最后的答案后,再判断此答案是否合法
合法则输出,否则输出AFK
*/
#include <algorithm>
#include <cstdio>
#include <queue>
#define int long long
using namespace std;
const int MARX = 1e6 + 10;
const int INF = 2147483647;
struct edge {
  int u, v, w, ne;
} a[MARX];
int n, m, b, maxcost;
int num;
int head[MARX], cost[MARX], cost1[MARX], dis[MARX];
bool vis[MARX] = {0, 1};
struct cmp1  //自定义优先级,优化SPFA
{
  bool operator()(const int a, const int b) { return dis[a] > dis[b]; }
};
priority_queue<int, vector<int>, cmp1> s;  //优先队列优化SPFA
//========================================================================
void add(int u, int v, int w) {
  a[++num].u = u, a[num].v = v, a[num].w = w;
  a[num].ne = head[u], head[u] = num;
}
bool judge(int x)  //判断是否合法
{
  if (cost[1] > x) return 0;  //起点花费是否大于x
  for (int i = 2; i <= n; i++) dis[i] = INF, vis[i] = 0;  // SPFA
  s.push(1);
  while (!s.empty()) {
    int top = s.top();
    s.pop();
    vis[top] = 0;
    for (int i = head[top]; i; i = a[i].ne) {
      int v = a[i].v, w = a[i].w;
      if (dis[v] > dis[top] + w && cost[v] <= x)  //限制一次收取的费用<=x
      {
        dis[v] = dis[top] + w;
        if (!vis[v]) {
          s.push(v);
          vis[v] = 1;
        }
      }
    }
  }
  return dis[n] < b;
}
signed main() {
  scanf("%lld%lld%lld", &n, &m, &b);
  for (int i = 1; i <= n; i++) {
    scanf("%lld", &cost[i]);
    cost1[i] = cost[i];
  }
  for (int i = 1; i <= m; i++)  //建图
  {
    int u, v, w;
    scanf("%lld%lld%lld", &u, &v, &w);
    add(u, v, w), add(v, u, w);
  }
  sort(cost1 + 1, cost1 + n + 1);
  int le = 1, ri = n, mid;
  while (le <= ri)  //二分答案
  {
    mid = (le + ri) >> 1;
    if (judge(cost1[mid]))
      ri = mid - 1;
    else
      le = mid + 1;
  }
  if (!judge(cost1[le]))
    printf("AFK");  //不可到达的情况
  else
    printf("%lld", cost1[le]);
}

一点也不难题

洛谷P1429 平面最近点对(加强版)

数据范围不是那么喜人,一个 \(\log\) 可过,考虑先固定一维,按横坐标排序后分治。
对于一个区间 \([l,r]\) 内的点对,递归处理左右半边内部的点对,仅考虑跨区间点对的贡献。
将当前统计到的,最小的距离 记为 \(ans\),设为全局变量。

考虑跨分界线的点对的贡献。
显然距离不小于 \(ans\) 的点对,必然是没有贡献的。
距离 \(<ans\) 的点对,其横坐标之差一定 \(<ans\)
又已按照横坐标进行了排序,考虑找到 最远的 与分界线横坐标差 \(<ans\) 的 跨区间的点对,它们及夹在它们之间的点对,都有可能对答案有贡献。
这样的点对可以简单二分获得,统计跨分界线的点对的贡献时,仅需统计这样的点对之间的距离即可。

复杂度比较玄学,但是跑得飞快。

//知识点:分治,二分
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 2e5 + 10;
const double kInf = 1e9 + 2077;
//=============================================================
struct Node {
  double x, y;
  bool operator < (const Node &sec_) const {
    return x < sec_.x;
  }
} node[kMaxn];
int n, llast, rlast;
double ans = kInf;
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Getmin(double &fir_, double sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
double distance(int i_, int j_) {
  double xi = node[i_].x, yi = node[i_].y;
  double xj = node[j_].x, yj = node[j_].y;
  return sqrt((xi - xj) * (xi - xj) + (yi - yj) * (yi - yj));
}
int Query(int l_, int mid_node, int r_) {
  llast = mid_node + 1;
  rlast = mid_node;
  double mid_nodex = node[mid_node].x;
  for (int l = l_, r = mid_node; l <= r; ) {
    int mid = (l + r) / 2;
    if (node[mid].x + ans > mid_nodex) {
      llast = mid;
      r = mid - 1;
    } else {
      l = mid + 1;
    }
  }
  for (int l = mid_node + 1, r = r_; l <= r; ) {
    int mid = (l + r) / 2;
    if (node[mid].x - ans < mid_nodex) {
      rlast = mid;
      l = mid + 1;
    } else {
      r = mid - 1;
    }
  }
}
void Solve(int l_, int r_) {
  if (l_ == r_) return ;
  int mid_node = (l_ + r_) / 2;
  Solve(l_, mid_node); Solve(mid_node + 1, r_);
  Query(l_, mid_node, r_);
  for (int l = llast; l <= mid_node; ++ l) {
    for (int r = rlast; r > mid_node; -- r) {
      Getmin(ans, distance(l, r));
    }
  }
}
//=============================================================
int main() {
  n = read();
  for (int i = 1; i <= n; ++ i) {
    scanf("%lf %lf", &node[i].x, &node[i].y);
  }
  std :: sort(node + 1, node + n + 1);
  Solve(1, n);
  printf("%.4lf", ans);
  return 0;
}

总结篇

学到了什么

二分查找:基于单调性的减治算法。
二分答案:基于答案单调性,加速枚举答案的算法。
三分:基于减治的求单峰函数区间内极值的算法。

P1873 砍树:简单的二分答案
P1824 进击的奶牛, P2658 汽车拉力比赛,P1462 通往奥格瑞玛的道路:二分答案套贪心,搜索,最短路。
P1429 平面最近点对(加强版):分治,使用二分查找优化枚举。


习题

P1873 砍树
P3382 【模板】三分法
P2249 【深基13.例1】查找
P1824 进击的奶牛
P2658 汽车拉力比赛
P1462 通往奥格瑞玛的道路
P1429 平面最近点对(加强版)

选做:
P1902 刺杀大使:二分答案+搜索
P2678 跳石头:二分答案+贪心
CF732D Exams:二分答案+贪心
P1485 火枪打怪:二分答案+前缀和(我刚学二分答案的时候倒是1h切了)


鸣谢

你们可爱的 yu__xuan 学姐。
https://www.luogu.com.cn/
https://oi-wiki.org/basic/binary/
https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%BC%94%E7%AE%97%E6%B3%95

posted @ 2020-09-11 18:30  Luckyblock  阅读(267)  评论(4编辑  收藏  举报