基础算法 02_二分法、三分法

更多二分题目戳我

二分是一种思想,而非模板

在什么情况下使用二分?需要同时满足两个条件:上下界[L,R]确定,函数在[L,R]内是单调的。

二分法模板:

int left = ?, right = ?;//给L和R确定边界
  while (left < right)
  {
    int ans;//ans记录答案
    int mid = (left + right) >> 1;
    if (check(mid)) {
      ans = mid;//记录答案
      ...//移动left(或者right)
    }
    else ...//移动right(或者left)
  }

所以,二分的难点就在于如何建立模型(寻找单调性作为mid)和check()条件(如何缩小边界),在写二分的时候,可能会用上其他的算法或者数据结构,比如在DP中,或者图论中。

二分法的典型应用有:最大值最小化最小值最大化

 

最大值最小化(让最大值尽可能的小)

https://www.luogu.com.cn/problem/P1462

题目:

题目背景
在艾泽拉斯大陆上有一位名叫歪嘴哦的神奇术士,他是部落的中坚力量

有一天他醒来后发现自己居然到了联盟的主城暴风城

在被众多联盟的士兵攻击后,他决定逃回自己的家乡奥格瑞玛

题目描述
在艾泽拉斯,有n个城市。编号为1,2,3,...,n。

城市之间有m条双向的公路,连接着两个城市,从某个城市到另一个城市,会遭到联盟的攻击,进而损失一定的血量。

每次经过一个城市,都会被收取一定的过路费(包括起点和终点)。路上并没有收费站。

假设1为暴风城,n为奥格瑞玛,而他的血量最多为b,出发时他的血量是满的。

歪嘴哦不希望花很多钱,他想知道,在可以到达奥格瑞玛的情况下,他所经过的所有城市中最多的一次收取的费用的最小值是多少。

输入格式
第一行3个正整数,n,m,b。分别表示有n个城市,m条公路,歪嘴哦的血量为b。

接下来有n行,每行1个正整数,fi。表示经过城市i,需要交费fi元。

再接下来有m行,每行3个正整数,ai,bi,ci(1<=ai,bi<=n)。表示城市ai和城市bi之间有一条公路,如果从城市ai到城市bi,
或者从城市bi到城市ai,会损失ci的血量。 输出格式 仅一个整数,表示歪嘴哦交费最多的一次的最小值。 如果他无法到达奥格瑞玛,输出AFK。 输入输出样例 输入 #1复制 4 4 8 8 5 6 10 2 1 2 2 4 1 1 3 4 3 4 3 输出 #1复制 10 说明/提示 对于60%的数据,满足n≤200,m≤10000,b≤200 对于100%的数据,满足n≤10000,m≤50000,b≤1000000000 对于100%的数据,满足ci≤1000000000,fi≤1000000000,可能有两条边连接着相同的城市。

  

分析:

题目说让求交费最多一次的最小值,是最大值最小化问题,我们二分这个费用。

check函数怎么写呢?
如果有比他大的,就换一条路走,走不通返回false
如果血量消耗完,返回false
但是这样显然是不对的,血量问题没有得到解决,血量够用不够用,
和路费的二分没有与之相匹配的单调性。
----------------------------------------------
我们换种思路来求,还是以路费作为二分的依据。
在所有的路径中,总有一个点的点权最大为x[]
我们要找到x[]中最小的那个x
我们将点权按照从小到大的顺序排个序,然后二分点权
check函数怎么写呢。
我们在二分到某个点权为x的点v的时候,将点权大于x的点删掉
在剩下的点中求一边最短路,如果不能从1走到n,返回false
如果求得的最短路的总边权大于等于我们的血量,返回false
然后都满足,返回true   

小计:

这个题,我做了大概有三个小时,分析这个题的时候,并没有多花时间(当然,也是因为看了其他人的博客,找了思路)。思路是对的,但是自己的编码能力确实不行,中间一直在错。
一开始,我想的是将每个点的点权从小到大排个序,然后枚举下标,l=0,r=n+1 。然后如果r在1到n之间的话,就是有解,否则就是没解。但是我忽略了在dijkstra算法的时候筛去哪些大于mid的值的时候,cost的下标早就已经变化了。
还有就是dijkstra用的居然如此的生疏,最开始明明不需要将st标记为true的,我给标记为true了,这里的st数组的作用是如果这个点已经扩展过了其他所有点,那就给他标记上。并不是spfa算法中的标记是否在队列中。
还有一点就是,记录图的时候,用strust还是不熟悉啊,用错了都。

 代码:

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>

using namespace std;

typedef pair<int, int> PII;

const int N = 100010, inf = 0x3f3f3f3f;

int n, m, blood;
int val[N];
int h[N], e[N], ne[N], w[N], tot;

void add(int a, int b, int c)
{
  e[++ tot] = b, w[tot] = c, ne[tot] = h[a], h[a] = tot;
}

int dist[N];
bool st[N];

bool check(int mid)
{
  // printf("%d\n", mid);
  memset(st, 0, sizeof st);
  memset(dist, 0x3f, sizeof dist);
  dist[1] = 0;
  priority_queue<PII, vector<PII>, greater<PII>> q;
  q.push({0, 1});

  while (q.size())
  {
    PII t = q.top();
    q.pop();

    int x = t.second;
    if (st[x]) continue;
    st[x] = true;

    if (val[x] > mid) continue;
    // printf("%d && %d && %d\n", mid, x, val[x]);

    for (int i = h[x]; ~i; i = ne[i])
    {
      int y = e[i], z = w[i];
      if (val[y] > mid) continue;
      if (dist[y] > dist[x] + z)
      {
        dist[y] = dist[x] + z;
        q.push({dist[y], y});
      }
    }
  }
  return dist[n] < blood;
}
int main()
{
  memset(h, -1, sizeof h);
  scanf("%d%d%d", &n, &m, &blood);
  for (int i = 1; i <= n; i ++ )
  {
    int x; scanf("%d", &x);
    val[i] = x;
  }

  for (int i = 1; i <= m; i ++ )
  {
    int x, y, z; scanf("%d%d%d", &x, &y, &z);
    add(x, y, z), add(y, x, z);
  }

  int l = 0, r = inf;
  if (!check(r))
  {
    puts("AFK");
    return 0;
  }
  while (l < r)
  {
    int mid = l + r >> 1;
    // puts("--------------------------");
    if (check(mid)) r = mid;
    else l = mid + 1;
  }

  printf("%d\n", r);
  return 0;
}

  

最小值最大化(让最小值尽量大)

 

题目:

题目描述
Farmer John建造了一个有N(2<=N<=100,000)个隔间的牛棚,这些隔间分布在一条直线上,坐标是x1,...,xN (0<=xi<=1,000,000,000)。

他的C(2<=C<=N)头牛不满于隔间的位置分布,它们为牛棚里其他的牛的存在而愤怒。为了防止牛之间的互相打斗,Farmer John想把这些牛安置在指定的
隔间,所有牛中相邻两头的最近距离越大越好。那么,这个最大的最近距离是多少呢? 输入格式 第1行:两个用空格隔开的数字N和C。 第2~N+1行:每行一个整数,表示每个隔间的坐标。 输出格式 输出只有一行,即相邻两头牛最大的最近距离。 输入输出样例 输入 #1复制 5 3 1 2 8 4 9 输出 #1复制 3

  

分析:

要让最小值最大化,我们二分的就是这个最小值,如果所有牛的间距都大于等于mid,就是合法的(那mid就是最小值)

我们的目标就是让这个mid更大,二分就好

代码:

 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 
 5 using namespace std;
 6 
 7 const int N = 100010, inf = 0x3f3f3f3f;
 8 
 9 int n, c;
10 int a[N], b[N];
11 
12 bool check(int mid)
13 {
14   int cnt = 1, sum = 0;
15   for (int i = 1; i <= n - 1; i ++ )
16   {
17     sum += b[i];
18     if (sum >= mid)
19     {
20       cnt ++;
21       sum = 0;
22     }
23   }
24 
25   return cnt >= c;
26 }
27 
28 int main()
29 {
30   scanf("%d%d", &n, &c);
31   for (int i = 1; i <= n; i ++ )
32   {
33     scanf("%d", &a[i]);
34   }
35 
36   sort(a + 1, a + 1 + n);
37   for (int i = 1; i < n; i ++ )
38   {
39     b[i] = a[i + 1] - a[i];
40   }
41 
42   int l = 0, r = inf;
43   while (l < r)
44   {
45     int mid = l + r + 1>> 1;
46     // printf("%d %d %d\n", mid, l, r);
47     if (check(mid)) l = mid;
48     else r = mid - 1;
49   }
50 
51   printf("%d\n", r);
52 
53   return 0;
54 }
View Code

 

三分法:

三分法常用来求解单峰函数或单谷函数的极值问题。

三分法通过将区间三等分(r - l) / 3,然后让mid1=l + (r - l) / 3, 让mid2 = r - (r - l) / 3;

如果这个函数是先增后减的,那么存在单峰。

如果f(mid1)<f(mid2)说明极值点v一定在mid1的右侧,如果f(mid1)>f(mid2)说明极值点一定在mid2的左侧。

三分法的时间复杂度是O(log(3))的。

 

https://www.luogu.com.cn/problem/P3382

题目:

题目描述
如题,给出一个 N 次函数,保证在范围 [l,r] 内存在一点 x,使得 [l,x] 上单调增,[x,r] 上单调减。试求出 x 的值。

输入格式
第一行一次包含一个正整数 N 和两个实数 l,r,含义如题目描述所示。

第二行包含 N + 1 个实数,从高到低依次表示该 N 次函数各项的系数。

输出格式
输出为一行,包含一个实数,即为 x 的值。若你的答案与标准答案的相对或绝对误差不超过 10^{-5} 则算正确。

输入输出样例
输入 #1复制
3 -0.9981 0.5
1 -3 -3 1
输出 #1复制
-0.41421
说明/提示
对于 100% 的数据,6<= N <=13,函数系数均在 [−100,100] 内且至多 15 位小数,∣l∣,∣r∣≤10 且至多 15 位小数.l <= r

【样例解释】



如图所示,红色段即为该函数 f(x) = x^3 - 3 x^2 - 3x + 1在区间 [-0.9981, 0.5]上的图像。

当 x = -0.41421 时图像位于最高点,故此时函数在 [l,x] 上单调增,[x,r] 上单调减,故 x=−0.41421,输出 −0.41421。

分析: 

实数三分模板题。

代码:

#include <cstdio>

using namespace std;

int n;
double l, r;
double a[55];

double f(double mid)
{
  double ans = 0;
  for (int i = n; i >= 0; i -- )
  {
    ans = ans * mid + a[i];
  }
  return ans;
}
int main()
{
  scanf("%d%lf%lf", &n, &l, &r);
  for (int i = n; i >= 0; i -- )
    scanf("%lf", &a[i]);

  while (r - l > 1e-6)
  {
    double mid1 = l + (r - l) / 3, mid2 = r - (r - l) / 3;
    if (f(mid1) > f(mid2)) r = mid2;
    else l = mid1;
  }

  printf("%.5lf\n", r);

  return 0;
}

三分:

[曲线](https://ac.nowcoder.com/acm/contest/951/C)

题目:

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

题目描述

明明做作业的时候遇到了n个二次函数Si(x)=ax2+bx+c,他突发奇想设计了一个新的函数F(x)=max⁡{Si(x)},i=1…n
明明现在想求这个函数在[0,1000]的最小值,要求精确到小数点后四位,四舍五入。

输入描述:

输入包含T组数据,每组第一行一个整数n;

接下来n行,每行3个整数a,b,c,用来表示每个二次函数的3个系数。注意:二次函数有可能退化成一次。

输出描述:

每组数据输出一行,表示新函数F(x)的在区间[0,1000]上的最小值。精确到小数点后四位,四舍五入。
示例1

输入

2
1
2 0 0
2
2 0 0
2 -4 2

输出

0.0000
0.5000

备注:

对于50%的数据,1≤n≤100;对于100%的数据,1≤T≤10,1n100000,0≤a≤100,0≤∣b∣≤5000,0≤∣c∣≤5000

分析:

最大值最小问题

唯一明白的就是F函数就是一个单谷函数。。。明白了这一点,就OK了

代码:

#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 100010, inf = 1 << 30;
const double eps = 1e-6;

int n;
double a[N], b[N], c[N];

double f(double x)
{
  double ans = -inf;
  for (int i = 1; i <= n; i ++ )
    ans = max(ans, a[i] * x * x + b[i] * x + c[i]);
  return ans;
}
void work()
{
  scanf("%d", &n);
  for (int i = 1; i <= n; i ++ )
  {
    scanf("%lf%lf%lf", &a[i], &b[i], &c[i]);
  }

  double l = 0, r = 1000;
  while (r - l > eps)
  {
    double d = (r - l) / 3;
    double mid1 = l + d, mid2 = r - d;
    if (f(mid1) > f(mid2)) l = mid1;
    else r = mid2;
  }

  printf("%.4lf\n", f(r));
}

int main()
{
  int T; scanf("%d", &T);
  while (T -- )
  {
    work();
  }
  return 0;
}

  三分套三分,我觉得你要偶尔性的回顾一下了

 

posted @ 2021-04-29 21:54  rookie161  阅读(507)  评论(0编辑  收藏  举报