比赛中常用的技巧——关于二分、倍增

二分

         先举一个例子:肯定很多人小的时候都玩过猜数字这个游戏,那么现在问题来了,如果在1-1000中去进行猜数字的游戏,那么我们最多要猜多少次,一些心急的人会说到1000次,如果按照顺序的方法去进行游戏的话,显然是1000次的,但是我们在实际操作中,真的有按照从1到1000的顺序去进行猜数字吗?肯定是不会的(除非你傻)。因为我们每一次得到的回答都是我们询问的数字与答案之间的大小关系,所以如果我们按照这个思路去进行二分的话,在最坏的情况下至少会猜10次,也就是log21000;具体的方法请看下面的代码。

int middle(void)
{
	int l=1;r=1001;
	while (l+1<r)
	{
		mid=(l+r)/2;
		if (mid<=ans) r=mid;
		else  l=mid+1;
	}
	return l;
}

           我们可以把二分的模型抽象的看成是一个长度为n的01数组,那么我们可以发现这个01数组一定是单调递增的:000000000000011111111111……我们每一次通过二分可以访问一个位置,我们要求第一个1的下标,那么问我们最少要去访问几次,那么很显然时间复杂度就是O(k log n)的,k是每次访问的代价。

    于是乎例子的问题就可以转换为是一个长度1000的数组,如果下标≤答案的就填成是0,而>答案的就填充为1,那么这个数组是单调递增的,而每次访问的代价是常数的时间。

    从这里我们就可以发现了二分的作用就是将时间复杂度优化,他可以将一个O(n)的算法变成是O(log n)的,他实现的思想就是每一次都把答案的范围缩小一半,知道最后只剩下一个答案为止。同时,二分是符合单调性的。

   我们通过二分答案,花费一个log的代价,将最优化的问题转化成了判定性的问题(这里有一点概念性)。

   我们看一下二分的经典问题:有n个数字,要求分成相邻的k组,使得每组数字之和的最大值最小,保证答案不超过10的9次方。(每个数都为非负整数)

我一眼看下来,完全看不到正解(我太菜了),于是我果断打了一个暴力。

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
using namespace std;

int maxx,n,k,a[1010];
bool tf[1010];

void dg(int x,int y)
{
	if (x==n+1 && y!=k-1) return;
	if (y==k-1)
	{
		int tot=0,ans=0;
		for (int i=1;i<=n;i++)
		{
			tot+=a[i];
			if (tf[i]==true)
			{
				if (tot>ans) 
				{
					ans=tot;
				}
				tot=0;
			}
		}
		if (tot>ans) ans=tot;
		if (ans<maxx) maxx=ans;
	}
	else 
	{
		tf[x]=true;
		dg(x+1,y+1);
		tf[x]=false;
		dg(x+1,y);
	}
}
int main()
{
	scanf("%d%d",&n,&k);
	for (int i=1;i<=n;i++) scanf("%d",&a[i]);
	maxx=2147483647;
	dg(1,0);
	printf("%d",maxx);
	system("pause");
	return 0;
}

        显然这种暴力做法的时间复杂度是十分高的,如果不是数据随机或者很水的情况下,我们很难拿得到分。而这道题恰巧可以用二分去做,我们把二分转化为另外一个问题,能否使得每组数字之和最大值是小于x的,那么这个问题贪心就可以了,这道题是满足单调性的,因为如果满足答案小于x,那么答案显然会小于x+1,所以我们可以二分找出最小的x,这个就是最终的答案。代码如下:

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
using namespace std;

int maxsum;
int tot,ans;
int n,k;
int a[1010];
int tt;


int middle(void)
{
	int l=1,r=maxsum,mid;
	while (l<r)
	{
		mid=(l+r)/2;
		tot=0;ans=0;
		for (int i=1;i<=n;i++)
		{
			if (tot+a[i]<=mid) tot=tot+a[i];
			else 
			{
				ans++;
				tot=a[i];
			}
		}
		ans=ans+1;
		if (ans<=k) r=mid;
		else l=mid+1;
	}
	return l;
}
int main()
{
	scanf("%d%d",&n,&k);
	for (int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		maxsum=maxsum+a[i];
	}
	tt=middle();
	printf("%d",tt);
	system("pause");
	return 0;
}

    同样我们也可以把这题转换成一个抽象的模型,这是一个长度为maxsum的数组,当下标≤ans的时候为0,当下标>ans的时候为1,符合单调性,找到第一个1的代价是O(n),这道题的总复杂度就变成了O(n log S)。

倍增

            首先我们了解一下什么是倍增,倍增的思想就是先预处理一个倍增数组,然后进行二进制分解之类的东西。他可以把时间复杂度从O(n)变成是O(log n)的。

    倍增的应用主要是快速幂,用O(log n)的时间计算x^n,预处理出x^1,x^2,x^4,x^8,x^16.(这里假设每个人都会倍增的思想,如果有不会的人可以先去看看别的资料),将n进行二进制分解,就举个例子吧,如果n=21,那么21=16+4+1对吧,那么x^n就等于x^16*x^4+x^1没错吧。那么这个时候的时间复杂度就变成了O(log n)了。

   倍增还可以应用于Sparse Table(什么东西?!),又叫做稀疏表。他是用来维护RMQ(又称区间最值)。给出一个长度为n的数组,每一次询问区间的最大值,没有修改。那么我们是不是就可以预处理从每个数开始,往后1个数字,2个数字,4个数字(以此类推)的最大值呢,我们把预处理的答案记为F数组,那么F[i][j]就表示从第i个数开始,往后2^j个数字的最大值,这里用了O(nlogn)的时间去预处理,当我们询问区间[L..R]的时候,设k为[1<<(R-L+1)],答案就是max(f[L][k],f[R-2^k+1][k]),我们再举个例子吧,对于四个数2,3,1,4;我们要查询[1..3]的区间最大值对吧,那么很显然答案就是max(f[1][1],f[2][1]);那么答案就是3;再看看比如我们要查询[2..4]的最大值,答案就是max(f[2][1],f[3][1]),也就是4。具体怎么操作的话看下表。

F[i][j]         

1            

2            

3                 

4                 

0                      

2

3

1

4

1                  

3

3

4

4

2                        

4

4

4

4

         我们看一下用倍增优化的具体的时间复杂度吧!如果是暴力的话那么预处理就是O(n)的,每一次询问也是O(n)的,这是比较简单的。如果是线段树的话预处理的时候时O(n)的,然后每一次询问用了O(log n)的时间,可是码量比较大(对于我这种蒟蒻来说),码力不太够啊!还有一种就是ST表,每一次预处理是O(n log n)的,然后单词询问是O(1)的,实现比较简单吧!

   还有一种倍增我不太熟悉,那就是树上倍增求LCA,因为本身我LCA就学的不好,准确点没学。那么这种倍增的时间复杂度预处理的话是O(nlogn),然后单词查询是O(logn)的,对于这种有根树,我们预处理出每个点上向上走1步,2步,4步(以此类推)走到的点,记为F数组。预处理出每一个点的深度(根节点的深度为0)。假设我们询问U,V两点的LCA的话。我们可以调整两点的深度相等,不妨设U深度神,设k=d[u]-d[v],然后将k二进制分解,向上走最多log次,然后从大到小去试探,得到LCA;就像下图一样。

    下面放一放一段代码:

void dfs(int u)
{
         if (v in child[u])
         {
                   d[v]=d[u]+1;
                   dfs(v);
         }
}
 
voidpreprocess(void)
{
         for (v in (1,n)) f[v][0]=fa[v];
         for (i in (1,logn))
         {
                   for (v in (1,n))
                   {
                            f[v][i]=f[f[v][i-1]][i-1];
                   }
         }
}
 
intlca(int u,int v)
{
         if (d[u]<d[v]) swap(u,v);
         for (i in (0,logn))
         {
                   if ((1<<i) &(d[u]-d[v])) u=f[u][i];
         }
         if (u==v) return u;
         for (i in (logn,0,-1))
         {
                   if (f[u][i]!=f[v][i])
                   {
                            u=f[u][i];
                            v=f[v][i];
                   }
         }
         return fa[u];
}

       别调了,这是伪代码!!!

        以上就是我对于倍增的一些简单介绍,但是我的肯定不是最全面的,所以推荐大家看下http://www.doc88.com/p-938465897306.html,这里面有详细的介绍。

  由于真的太菜,所以每天改题都用了很多的时间,倍增和简单分块来不及打了,以后会更新。因为本文是一个蒟蒻写的,再加上没有人帮我验代码,所以将就着看一看,理解一下思路就可以了,如果有大神发现我哪里写错了,欢迎来拍我一巴掌!!!!!感谢phx大犇给予了使我恍然大悟的言论,谢谢phx同学!!!!!!!!!

猜你喜欢

转载自blog.csdn.net/gao_jue_yi/article/details/77387777