基础算法系列 —— 有关二分答案法的玄学技巧

       有一种可以用于解一大类问题的通用方法:二分答案。二分答案的应用范围非常广,广到什么程度呢?大概就是,值得在面对每一道新题目的时候,都先确认一下这道题是否可以用二分答案解决的程度,因为它能够有质地优化时间复杂度,同时又具有暴力枚举答案的美感,简单易懂。

       👆:基本二分模板

          👆:两种浮点二分(有点像计算几何的精度处理呢)

        二分答案的基本步骤就是,二分枚举答案 —— 即在答案的范围内二分查找,然后一般采用一个check ( ) 函数或者 ok ( ) 函数检验答案的正确性,这类题能够使用二分答案法一般有一个前提,那就是能够在知道此时这个答案的正确性后是否能正确地决定下一次答案的范围?~🤔,所以一般二分答案法的问题都是有明显特征的,差不多就是以下三种(一般来讲):

        那么其实在这篇文章中,相信大家二分答案法的题目已经做过不少了,所以不是主讲二分答案是基本方法了,而是对于几个小技巧的讲解,主要是在解题过程中的心得,希望对大家能有所帮助~(本文章对于算法掌握度有一定的要求,所讲解的题目算法综合性较强)那么在这一部分中,主要分享两种(说不定是歪门邪道):

      第一是:☆二分答案法的检验函数可玩性很强;

      第二是:★二分答案法的方向可以自己决定,对某些题具有纠错能力。

          1.检验函数什么都可以放~

         先来一个简单一点的✌

         问题是这样的:

        现在有NxN个水池,每个水池都有一个海拔高度。天开始下雨,在时间为t的时候,任何一个      水池的深度为t。如果你想从一个水池游到相邻的另外一个水池,那么这水池的深度必须大于水      池的海拔,即这个水池中必须有积水才行。
         假设你可以在瞬间游无穷远的距离,现在你从左上角的水池开始游,请问你最少需要花多少       时间才能游到右下角的泳池。

         例如:

               4
               1  2  3  4
               4  3  2  1
               4  2  1  1
               1  2  3  2

           最少时间是4。

        当时间为4的时候,你可要从( 1 , 1 )开始顺利的游动到( 4 , 4 )这个位置。
        其中一条参考路线:( 1 , 1) -> ( 1 , 2 ) -> ( 1 , 3 ) -> ( 2 , 3 ) -> ( 2 , 4 ) -> ( 4 , 3 ) -> ( 4 , 4 )
        经过的每个地方水深为4,很明显都大于经过的水池的海拔。

        [输入]
          第一行一个整数N. (0<=N<=80)
          第二行到第N+1行,每行N个整数(0<=水池海拔<=N*N-1)。

       思路点拨:

          我们发现,这个水池的长宽非常小,但是海拔有一定高度,所以直接枚举这个正确答案是肯      定超时的,并且经过我们的冥思苦想然而没有发现有关数学的巧妙思路,因此我们仔细观察这        道    题目,发现所需枚举的答案是符合二分答案法的条件的,所以我们赶紧打出一个模板~:

         👆:主程序😄

          那么我们如何检验我们得到的答案呢,那就是让这个答案在泳池里面游一遍,看看能否顺利游到终点,这边我们只需要把检验对象加到海拔上就OK辣~,所以检验程序中我们直接来一遍BFS(广度优先搜索)几乎是套模板了,只能说这种检验函数有一点点少见:

     OK,通过二分答案法,我们又切了一题,接下来我们上点抽象的~😄

            来看看题~      2728 -- Desert King

       这道题呢其实把它简化出来的意思就是,给你一张有n个点的无向图,每两个点之间都有一条边,题目中的点与点之间的距离是第一个权值我们称之为Ui,然后村庄与村庄之间的海拔高度差则为第二权值我们称之为Wi,然后要求我们从中找到一棵树,要求   \frac{\sum wi}{\sum ui}  这个值呢最小(或者最大,也可以有这种情况),那么我们一般看到这样的题目一般肯定是往图论算法方面想了,当然把每条边分别求值最后求最小生成树的算法肯定是错的,那么我们话不多说,其实啊,这道题目可以用二分答案,没错真的是可以的!~ 这种树在树论中我们称之为最优比例生成树!

     那么我们来思考二分答案这个算法怎么想🤔,这道题目里我们是求最小,所以我们假设最小的答案为best,我们二分的答案为ans。那么我们将每条边的边权变为Wi - Ui * ans。则:

           ans < best 时,求最小生成树得到的答案大于0;

           ans = best 时,求最小生成树得到的答案等于0;

           ans > best 时,求最小生成树得到的答案小于0。

                                                     \frac{\sum wi}{\sum ui} = ans

          (w1 + w2 + ... wn-1) - (u1 + u2 + ...un-1) * ans= \sum wi - ui * ans

  所以按照这个思路我们只需要在check函数中写一个prim板子求最小生成树就行了👇:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e3 + 20;
int n,i,j,x,y,z;
double ma1[MAXN][MAXN],ma2[MAXN][MAXN];
double dist[MAXN << 1];
bool p[MAXN << 1];
#define Temp template<typename T>
Temp inline void read(T &x)
{
	x = 0;
	T w = 1,ch = getchar();
	while(! isdigit(ch) && ! ch == '-')
	    ch = getchar();
	if(ch == '-')
	    w = -1,ch = getchar();
	while(isdigit(ch))
	    x = (x << 3) + (x << 1) + (ch ^ '0'),ch = getchar();
	x = x * w;
}
struct Villages{ int x,y,h; }villages[MAXN << 1];
int sqr(int x) { return x * x;}
double Dis(int x1,int yy,int x2,int y2) { return sqrt(sqr(x1 - x2) + sqr(yy - y2));}
double check(double data)
{
	int tj;
	double temp,ans;
	for(int i = 2;i <= n;i++)
	{
		p[i] = false;
		dist[i] = ma2[1][i] - data * ma1[1][i];
	}
	ans = 0;
	p[1] = true;
	dist[1] = 0;
	for(int i = 1;i <= n - 1;i++)
	{
		temp = 1e30;
		tj = 0;
		for(int j = 2;j <= n;j++)
		if(p[j] == false && dist[j] < temp)
		{
			tj = j;
			temp = dist[j];
		}
		ans = ans + dist[tj];
		p[tj] = true;
		for(int j = 2;j <= n;j++)
		if(p[j] == false) dist[j] = min(dist[j],ma2[tj][j] - ma1[tj][j] * data);
	}
	return ans;
}
int main()
{
	while(true)
	{
		read(n);
		if(n == 0) break;
		for(int i = 1;i <= n;i++)
		{
			read(x),read(y),read(z);
			villages[i].x = x,villages[i].y = y,villages[i].h = z;
		}
		memset(ma1,0,sizeof(ma1));
		memset(ma2,0,sizeof(ma2));
		for(int i = 1;i <= n;i++)
		for(int j = 1;j <= n;j++)
		if(i == j) 
		{
			ma1[i][j] = ma1[j][i] = 0;
			ma2[i][j] = ma2[j][i] = 0;
		} else
		{
			
			ma1[i][j] = ma1[j][i] = INT_MAX;
			ma2[i][j] = ma2[j][i] = INT_MAX;
		}
		for(int i = 1;i <= n;i++)
		for(int j = 1;j <= n;j++)
		{
			ma1[i][j] = ma1[j][i] = Dis(villages[i].x,villages[i].y,villages[j].x,villages[j].y);
			ma2[i][j] = ma2[j][i] = abs(villages[i].h - villages[j].h);
		}
		double l,mid,r;
		l = 0,r = 1e6;
		for(int i = 1;i <= 50;i++)
		{
			mid = (l + r) / 2;
			if(check(mid) < 0) r = mid;else l = mid;
		}
		printf("%.3f\n",(l + r) / 2);
	}
	return 0;
}

      其实这些check函数在ACM-ICPC这些比赛中的二分答案法题目里真不少见,其实也是我们必须要掌握的,鉴于题目所考察的算法高度综合,流行数据结构和二分答案相结合😄,比如下面2023年ICPC杭州站的F题👇:

    👆:有兴趣的读者可以好好思考一下这道题[doge]

   2.技巧:★二分答案法的方向可以自己决定

       我们碰到某些神奇的问题的时候,发现这道题目非常适合二分又明显不符合条件,然后不用二分就发现要手敲各种数据结构了,总是在想,有没有一种方法能够强行让这道题目二分解出🤔~

      今天在这里我就同一道题目三种解法,包含两种用来强行纠错的二分类型。

问题描述


      某个帝国修了一条非常非常长的城墙来抵御外敌,城墙共分n段,每一段用一个整数来描述坚固程度。
过了几百年,城墙年久失修,有很多段都已经损坏,于是皇帝决定派你去修理城墙,但是经费有限。
所以你准备先考察一下城墙。如果一段连续的城墙它们的坚固程度之和>0,那么我们认为这段城墙暂时有效。

例如
5
-5 4 -3 2 3
这段城墙共分5段,坚固程度之和=1,要比0大,我们认为它还算有效。
下面告诉你n段城墙的坚固情况。
请你求出最长的一段连续的城墙,要求坚固程度之和>0

输入
第一行是一个整数n。
第二行共n个整数,分别描述每一段城墙的坚固程度
输出
一行一个整数表示长度。

样例:

输入

10
-1 0 -1 2 0 -1 -1 2 -1 0

输出

7
样例说明:
即,从第2个段开始,到第8段结束

100%的数据 n<=100000,且描述城墙坚固程度的整数的范围-10^8到10^8之间

       咋一看这道题目颇有可以二分答案的样子~ ,我们看看二分答案的可行性:

         我们发现,检验答案的正误后,无论正确与否,因为有负数的存在,我们都不知道正确的合法的最长序列到底比这个长还是比这个短。(拿样例数据模拟就可以明显发现)所以之后一般就不会往二分答案考虑了,这样子就错失良机了/(ㄒoㄒ)/~~

       其实我们仔细想想就可以发现,二分的检验函数出了问题,直接单纯检验这一个答案的正误显然是不够的,我们还需要了解序列变长的时候是否还存在正确答案,因此我们首先想到ST表处理👇(不用细看,理解就好,还有更简单的!):

#include<bits/stdc++.h>
using namespace std;
long long a[100005],f[100005][25];
int n,ans,i;
bool check(int left,int right)
{
	int p=log2(right-left+1);
	if (max(f[left][p],f[right-(1<<p)+1][p])>a[i-1])
		return 1;
	return 0;
}
int main()
{
	scanf("%d",&n);
	for(i=1;i<=n;i++)
	{
		int x;
		scanf("%d",&x);
		a[i]=a[i-1]+x;
		f[i][0]=a[i];
	}
	for(int k=1;k<=log2(n);k++)
		for(i=1;i<=n-(1<<k)+1;i++)
			f[i][k]=max(f[i][k-1],f[i+(1<<k-1)][k-1]);
	for(i=1;i<=n;i++)
	{
		if (check(i,n)==0)
			continue;
		int left=i,right=n;
		while (left<right)
		{
			int mid=(left+right+1)/2;
			if (check(mid,right))
				left=mid;
			else
				right=mid-1;
		}
		ans=max(ans,left-i+1);
	}
	printf("%d\n",ans);
	return 0;
}

       但是其实这样子略显麻烦了,我们明显不需要ST表写动规,只需要对前缀和这个数组求出一个前缀最小数组,然后每次判断在之前更长的序列中是否存在一个大于等于零的序列,如果存在则下一次二分方向往大走,如果没有那我们就直接结束辣~★

        👇这样子代码明显简化了很多:

#include<bits/stdc++.h>
using namespace std;
long long i,n,mid,l,r,a[100010],f[100010],ans,Max,h[100010],minn;
bool p;
bool check(long long mid)
{
	for(int i = 1;i <= n - mid + 1;i++)
		if(f[i + mid - 1] - h[i - 2] > 0) p = true;
	for(int i = 1;i <= n - mid + 1;i++)
		if(f[i + mid - 1] - f[i - 1] > 0) return 1;
	return 0;
}
int main()
{
	cin>>n;
	for(i = 1;i <= n;i++)
	{
		scanf("%lld",&a[i]);
		f[i] = f[i - 1] + a[i];
	}
	for(int i = 1;i <= n;i++) h[i] = INT_MAX;
	minn = INT_MAX;
	for(int i = 1;i <= n;i++)
	{
		if(f[i] < minn) minn = f[i];
		h[i] = minn;
	}
	h[0] = h[ -1] = 0;
	ans = 1;
		l = 1;r = n;
		while(l <= r)
		{
			p = false;
			mid = (l + r) / 2;
			if(check(mid))
			{
				if(mid > ans) ans = mid;
				if(p == true) l = mid + 1;else r = mid - 1;
			}
			else if(p == true) l = mid + 1;else r = mid - 1;
		}
	cout<<ans<<endl;
	return 0;
}

        OK,这道题目的正解我们已经非常清晰地写出,那么是否还有二分答案法修正的神奇方法呢🤔?

         假如单纯的二分会错失检验正确答案的良机,那么还有什么办法能够扫到这个答案呢?那我们就来赌一把,一次二分的概率可能是0.5的指数倍,那假如我们多做一次二分呢🤔?,是不是概率会有所上升?所以这里就推出了一个新的技巧:扩大边界然后做若干次二分答案法,每次的最大上界都不同(这个算法纯属打开思路,本题雀实通过了,但是正确性有待证明),我们只需要在二分答案法的外面加一个循环,做个几百次,赌它一定能检测到正确答案~,代码非常简单👇:

#include<bits/stdc++.h>
using namespace std;
long long i,n,mid,l,r,a[100010],f[100010],ans,Max;
bool check(long long mid)
{
	for(int i = 1;i <= n - mid + 1;i++)
	{
		if(f[i + mid - 1] - f[i - 1] > 0)return 1;
	}
	return 0;
}
int main()
{
	srand(time(NULL));
	cin>>n;
	for(i = 1;i <= n;i++)
	{
		scanf("%lld",&a[i]);
		f[i] = f[i - 1] + a[i];
	}
	for(i = 1;i <= 100;i++)
	{
		l = 1;r = n * i;
		while(l <= r)
		{
			mid = (l + r) / 2;
			if(check(mid))
			{
				ans = mid;
				l = mid + 1;
			}
			else r = mid - 1;
		}
		if(ans > Max) Max = ans;
	}
	cout<<Max;
	return 0;
}

      注释:srand(time(NULL));这句话就是给程序加上了随机性的,所以这种二分答案又可以说是随机二分~

     关于二分答案法的各种小技巧就分享到这里,二分答案法的用途可以说是非常的广,在决策单调性上可以说是屡试不爽,尤其是二分栈和二分队列,不过其底层思路还是分治思想,本文主要是为了打开思路,可能内容不是很多也有些杂[doge],那就先讲到这里~

  • 28
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值