所谓分治,便是分而治之——题记
一、分治
本质:
(注:当n=2是又称为二分)
接下来附上百度上贴的步骤:
用图解释
那么从这张图上就可以看到分治的主要实现方法就是递归,先把原问题分成若干个小问题,再一次求出每个小问题的解,再反上去用每个小问题合成原问题的解。这便是一个典型的递归的过程。(关于递归我会下次再说)
那么分治有以下优美的模板
if 问题不可分{
直接求解;
返回问题的解;
}
else {
对原问题进行分治;
递归对每一个分治的部分求解
归并整个问题,得出全问题的解;
};
好吧也许不够优美
那么下面给出一道例题:
例题0:
题目简述:现在有n块不同,有一台比较重量的仪器,我们希望用最少的比较次数找出最轻和最重的金块。
分析:emmm有大佬说:“暴力啊!” 。。(确实是暴力,把你暴力揍一顿。)
我们先来想暴力的解法:直接一重for 枚举砝码重量,同时开一个maxx和minn来记录最大值和最小值。
主要代码如下:
for (int i=1;i<=n;i++)
maxx=max(maxx,a[i]),minn=min(minn,a[i]);
有人会说:“这个效率很高啊!O(n)呢!”,是很大,可是万一数据一大呢?过亿呢(暂不考虑输入的时间)?是不是就超时了呢?
那么我们接下来说分治的思路:n≤2,识别出最重和最轻的金块,一次比较就足够了。
n>2,第一步,把这袋金块平分成两个小袋A和B。第二步,分别找出在A和B中最重和最轻的金块。设A中最重和最轻的金块分别为HA 与LA,以此类推,B中最重和最轻的金块分别为HB 和LB。第三步,通过比较HA 和HB,可以找到所有金块中最重的;通过比较LA 和LB,可以找到所有金块中最轻的。在第二步中,若n>2,则递归地应用分而治之方法。
具体图示如下:
还可以这样:
这里代码就不贴了(其实是懒得现场写了)
例题1:给定一个n,打印出一下规律的表格:
1 2 3 4 5 6 7 8
2 1 4 3 6 5 8 7
3 4 1 2 7 8 5 6
4 3 2 1 8 7 6 5
5 6 7 8 1 2 3 4
6 5 8 7 2 1 4 3
7 8 5 6 3 4 1 2
8 7 6 5 4 3 2 1
分析:我们进行找规律,不难发现:
以表格的中心为拆分点,将表格分成A、B、C、D四个部分,就很容易看出有A=D,B=C,并且,这一规律同样适用于各个更小的部分。设有n个选手的循环比赛,其中n=2m,要求每名选手要与其他n-1名选手都赛一次。每名选手每天比赛一次,循环赛共进行n-1天。要求每天没有选手轮空.以下是八名选手时的循环比赛表,表中第一行为八位选手的编号,下面七行依次是每位选手每天的对手。
从八位选手的循环比赛表中可以看出,这是一个具有对称性的方阵,可以把方阵一分为四来看,那么左上角的4*4的方阵就是前四位选手的循环比赛表,而右上角的4*4的方阵就是后四位选手的循环比赛表,它们在本质上是一样的,都是4个选手的循环比赛表,所不同的只是选手编号不同而已,将左上角中方阵的所有元素加上4就能得到右上角的方阵.下方的两个方阵表示前四位选手和后四位选手进行交叉循环比赛的情况,同样具有对称性,将右上角方阵复制到左下角即得到1,2,3,4四位选手和5,6,7,8四位选手的循环比赛表,根据对称性, 右下角的方阵应与左上角的方阵相同.这样,八名选手的循环比赛表可以由四名选手的循环比赛表根据对称性生成出来.同样地, 四名选手的循环比赛表可以由二名选手的循环比赛表根据对称性生成出来,而两名选手的循环比赛表可以说是已知的,这种程序设计方法叫做分治法,其基本思想是把一个规模为n的问题分成若干个规模较小的问题,使得从这些较小问题的解易于构造出整个问题的解。
程序中用数组matchlist记录n名选手的循环比赛表, 整个循环比赛表从最初的1*1的方阵按上述规则生成出2*2 的方阵, 再生成出4*4 的方阵,……,直到生成出整个循环比赛表为止.变量half表示当前方阵的大小,也是要生成的下一个方阵的大小的一半 。
那么具体代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,a[201][201]={};
int main(){
scanf("%d",&n);
a[1][1]=1;
int k=1;
while(n--){
for (int i=1;i<=k;i++)
for (int j=1;j<=k;j++)
a[i][j+k]=a[i][j]+k;//累加
for (int i=1;i<=k;i++)
for (int j=1;j<=k;j++)
a[i+k][j+k]=a[i][j];
for (int i=1;i<=k;i++)
for (int j=k+1;j<=2*k;j++)
a[i+k][j-k]=a[i][j];//赋值
k*=2;
}
for (int i=1;i<=k;i++){
for (int j=1;j<=k;j++)
printf("%3d",a[i][j]);
printf("\n");
}
return 0;
}
(好吧就是一道例题
二、二分
二分是分治中的一个特殊的分支,一般用的较多
在说二分之前,我们先讨论一下线性与二分的效率差别(小数据差别不大,大数据就能体现了)
1、n=10 线性:10 二分:≈4
2、n=1000 线性: 1000 二分:≈10(已经逐渐体现)
3、n=1000000 线性:1000000 二分:≈20(我屮艸芔茻这简直就是天和地的区别啊)
4、n=10000000000 线性:10000000000 二分:<100 (…………无语)
那么从以上几组数据就可以看出二分和线性的效率了吧,这也可以给某一些认为二分效率和线性效率差不多的认狠狠的抽一个巴掌了(啪啪啪!哈哈哈哈哈嗝)
i:二分查找
二分查找是二分中的一类(其实也差不多),其主要实在一个具有单调性的序列中查找等于或与x最接近的值。但这种题目的数据一般都会过亿,直接卡爆线性。
主要代码如下:
//假设单调递增
while (l+1<r){//最为保险的二分
int mid=(l+r)/2;
if (a[mid]==x) return mid;
if (a[mid]>x) r=mid;
else l=mid;
}
if (a[l]==x) return l;
if (a[r]==x) return r;
return -1;
一般考试不会考裸的二分查找,一般会与二分答案一起,那么接下来说二分答案
ii:二分答案
顾名思义,就是将答案不停的二分,知道不能再分,在慢慢返回去求出原问题的解,即最有答案
下面上几道例题:
1、擦护栏
题目简述:
Jzyz的所有学生获得了一个大扫除的机会——去大街擦护栏。 Jz市大街被分成了M段,每段的长度不一定相同。Jzyz一共有N名学生参加劳动,这些学生将分成M组来完成这项工作,因为长度不同,分配的任务量肯定不同,现在,负责这次大扫除的老师想知道,擦护栏长度最多的同学最少必须擦多长的护栏,这样才能保证尽可能的公平。当然,可以有学生当拉拉队,不用擦护栏,但是每段护栏必须要擦干净。 比如:有5名学生,2段护栏,第一段长度为7,第二段长度为4.可以让3个人负责擦长度为7的,2个人负责长度为4的,那么擦第一段的某个人必须要擦长度为3 的护栏,而其他的人擦长度为2 的护栏。这样就有1位同学必须擦长度为3的护栏。
输入格式:
第一行:两个整数N和M(1 ≤ N ≤ 10^9) (1 ≤ M ≤ 300 000, M ≤ N)
接下来M行,每行一个整数,表示每段护栏的长度,保证护栏的长度在[1,10^9] 之间。
输出格式
一个整数,如题目描述,任务量最重的同学擦护栏长度的最小值。
暴力分析:对于这题,我们先想一想暴力的做法:循环枚举每一个长度,检验长度是否合理,若合理,与maxx去一个较大值。
效率分析:eng 看起来效率不错,O(n)(这还是舍去校验部分的效率),但你看看数据。。10^9..哈哈哈,刚刚就是卡掉暴力分。。
二分分析:这题很显然可以用二分(不需要证明吧。。)。我们可以二分木板的长度,如果当前木板长度符合要求,那么就可以继续查找尝试着左区间(因为是要较小值),否则就查找右区间,寻找较大值。
这样一种递归下去便可,最后在校验l和r哪个合理便可(这里用的是规定的模板,是最可靠的)
具体代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[1000001]={};
inline bool f(int k){
int ans=n;//先给每组的人都分一个k的工作
int x=1;
while (x<=n) ans+=a[x]%k==0?a[x]/k-1:a[x]/k,x++;//累加人员
return ans<=m;
}
int main(){
scanf("%d%d",&m,&n);
for (int i=1;i<=n;i++) scanf("%d",&a[i]);
int l=1,r=1000000001;
while(l+1<r){
int mid=(l+r)/2;
if (f(mid)) r=mid;//如果可以,则尝试寻找较小值
else l=mid+1;//否则就寻找较大值
}
printf("%d",f(l)?l:r);//注:这里必须先检验l,即较小值,因为题目中所求的是最大的最小,默认最小(也许吧)
return 0;
}
二、烤鱼
题目简述:
小x掉到了一个jzyz的窨井里,这口井很深,没有人能听到小x的呼救,悲催的小x必须要等D天后,学校确认他失踪才会大规模寻找他,而这难熬的D天将是小x这一生最难过的D天。 不过幸运的是小x在井里得到了N (1 <= N <= 50,000) 条鱼,编号1..N.他计划在接下来的D (1 <= D <= 50,000)天按鱼的编号从小到大逐天吃,当然,小x可以一天连续吃多条鱼,也可以不吃。 为了不浪费,小x要求鱼必须吃完。。 对于第i条鱼,小x的能量值会增加Hi(1 <= Hi <= 1,000,000)。小x会在白天吃鱼,一旦吃饱,他就会一觉睡到第二天。第二天醒来,她的能量值会变成前一天的能量值的一半(向下取整),小x的能量值是可以累加的。 小x比较注意维护每天能量的平衡,要刻意避免能量的大起大落,于是现在小x想知道,如何安排吃鱼,才能保证能量值最小的那个晚上(假设是第X个晚上),第X个晚上的能量值最大。 例如:有5条鱼,能量值分别是: (10, 40, 13, 22, 7). 小x按以下方式吃鱼,将会得到最优值:
·第几天 | ·醒来时能量值 | ·吃了鱼后能量值 | ·晚上睡觉能量值· | |
---|---|---|---|---|
1 | 0 | 10+40 | 50 | |
2 | 25 | --- | 25 | |
3 | 12 | 13 | 25 | |
4 | 12 | 22 | 34 | |
5 | 17 | 7 | 24 |
可以看出,第1天吃了鱼1、鱼2,第2天不吃鱼,第3天吃了鱼3,第4天吃了鱼4,第5天吃了鱼5。 那么在这5个晚上,能量值最低的是第5个晚上,能量值是24,所以输出24。然后你还要输出第I (1<=i<=N)条鱼是小x第几天吃掉的。
输入格式:
第1行:两个整数: N 和 D 第2..N+1行: 每行一个整数Hi。
输出格式:
第1行: 一个整数, 在这D个晚上里,能量值最小的那个晚上(假设是第X个晚上),第X个晚上的能量值最大可以是多少? 第2.....N+1行: 每行一个整数,第 i 行表示第i条鱼是第几天被吃的。
分析:这道题的二分是藏得有点深的(要不是题目标签有着大大的“二分答案”几个字,我完全不会想到这是一题二分)。我们可以二分枚举能量值,在判断能量值是否可行,在用一个数组存储天数即可。
那么具体代码如下:
#include<bits/stdc++.h>
using namespace std;
long long n,d;
long long a[10000001]={};
long long b[10000001]={};
inline bool f(long long x){
long long num=0;//记录吃掉的烤鱼的编号
long long ans=0;//能量值
for (long long i=1;i<=d;i++){
ans/=2;//能量值减半
while (ans<x&&num<=n){
++num;//下一条鱼
ans=ans+a[num];//累加能量
if (num>n) return 0;
}
}
return ans>=x;
}
int main(){
long long maxx=0;
scanf("%lld%lld",&n,&d);
for (long long i=1;i<=n;i++) scanf("%lld",&a[i]),maxx+=a[i];
long long l=1,r=maxx;
while (l+1<r){
// printf("%d %d\n",l,r);
long long mid=(l+r)/2;//二分能量值
if (f(mid)) l=mid;//尝试更大值
else r=mid;//寻找较小值
}
printf("%lld\n",f(r)?r:l);
long long x=f(r)?r:l;
long long len=0,ans=0;
for (long long i=1;i<=d;i++){
ans/=2;//能量值减半
while (ans<x){//如果还没超出当前的最大能量值
ans+=a[++len];//累加能量
b[len]=i;//存储天数
}
}
for (long long i=1;i<=len;i++) printf("%lld\n",b[i]);//输出
while (len<n) len++,printf("%lld\n",d);//(注意这一步),如果烤鱼没吃完,那就全放在最后一天吃
return 0;
}