Professional Documents
Culture Documents
第7章 分治算法 (C++版)
第7章 分治算法 (C++版)
所谓分治就是指的分而治之,即将较大规模的问题分解成几个较
小规模的问题,通过对较小规模问题的求解达到对整个问题的求解。当
我们将问题分解成两个较小问题求解时的分治方法称之为二分法。
你们玩过猜数字的游戏吗?你的朋友心里想一个 1000 以内的正整
数,你可以给出一个数字 x ,你朋友只要回答“比 x 大”或者“比 x
小”或者“猜中”,你能保证在 10 次以内猜中吗?运气好只要一次就
猜中。
开始猜测是 1 到 1000 之间,你可以先猜 500 ,运气好可以一次猜
中,如果答案比 500 大,显然答案不可能在 1 到 500 之间,下一次猜测
的区间变为 501 到 1000 ,如果答案比 500 小,那答案不可能在 500 到
1000 之间,下一次猜测的区间变为10002
1到
1 499 。只要每次都猜测区间的
10
中间点,这样就可以把猜测区间缩小一半。由于 ,因此不超过 10
次询问区间就可以缩小为 1 ,答案就会猜中了,这就是二分的基本思想。
n
每一次使得可选的范围缩小一半,最终使得范围缩小为一个数,
2x 2
n
1得x log
{
x1=(x-0.05)/100 ; x2=(x+0.05)/100 ; // 在题目要求的精度内设定区
间
if (f(x1)*f(x2)<0||f(x1)==0) // 若在区间两端的函数值异号或在 x1 处
的
函数值为 0 ,则确定 x/100 为根
printf(“%.2f”,x/100) ;
}
其中函数 f(x) 计算 x3+b*x2+c*x+d :
double f(double x) // 计算 x3+b*x2+c*x+d
{
2. 分治法
枚举根的值域中的每一个整数 x(-100≤x≤100) 。由于根与根之
差的绝对值≥ 1 ,因此设定搜索区间 [x1 , x2] ,其中 x1=x , x2=x+1 。
若
⑴f(x1)=0 ,则确定 x1 为 f(x) 的根;
⑵f(x1)*f(x2)>0 ,则确定根 x 不在区间 [x1 , x2] 内,设定
[x2 , x2+1] 为下一个搜索区间
⑶f(x1)*f(x2)<0 ,则确定根 x 在区间 [x1 , x2] 内。
如果确定根 x 在区间 [x1 , x2] 内的话( f(x1)*f(x2)<0 ),如何
在该区间找到根的确切位置。采用二分法,将区间 [x1 , x2] 分成左右两
个子区间:左子区间 [x1 , x] 和右子区间 [x , x2] (其中 x= ):
如果 f(x1)*f(x)≤0 ,则确定根在左区间 [x1 , x] 内,将 x 设为该
区间的右指针( x2=x ),继续对左区间进行对分;如果 f(x1)*f(x)>0 ,
则确定根在右区间 [x , x2] 内,将 x 设为该区间的左指针( x1=x ),
继续对右区间进行对分;
上述对分过程一直进行到区间的间距满足精度要求为止( x2-
x1<0.001 )。此时确定 x1 为 f(x) 的根。
由此得出算法:
输入方程中各项的系数 a , b , c , d ;
{
for (x=-100;x<=100;x++) // 枚举每一个可能的根
{
x1=x;x2=x+1; // 确定根的可能区间
if (f(x1)==0) printf("%.2f ",x1); // 若 x1 为根,则输出
else if (f(x1)*f(x2)<0) // 若根在区间 [x1 , x2] 中
{
while (x2-x1>=0.001) // 若区间 [x1 , x2] 不满足精度要求,则循环
{
xx=(x2+x1)/2; // 计算区间 [x1 , x2] 的中间位置
if ((f(x1)*f(xx))<=0) // 若根在左区间,则调整右指针
x2=xx;
else x1=xx; // 若根在右区间,则调整左指针
}
printf("%.2f ",x1); // 区间 [x1 , x2] 满足精度要求,确定 x1 为根
}
}
cout<<endl;
}
double f(double x) // 将 x 代入函数
{
return (x*x*x*a+b*x*x+x*c+d);
【例 4 】、循环比赛日程表( match )
【问题描述】
设有 N 个选手进行循环比赛,其中 N=2M ,要求每名
选手要与其他 N-1 名选手都赛一次,每名选手每天比赛一次,
循环赛共进行 N-1 天,要求每天没有选手轮空。
输入: M
输出:表格形式的比赛安排表
【样例输入】 match.in
3
【样例输出】 match.out
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
【问题分析】
以 M=3( 即 N=23=8) 为例,可以根据问题要求,制定出如下图
所示的一种方案:
以表格的中心为拆分点,将表格分成 A 、 B 、 C 、 D 四个部分,就
很容易看出有 A=D , B=C ,并且,这一规律同样适用于各个更小的部分。
设有 n 个选手的循环比赛,其中 n=2m ,要求每名选手要与其他 n-1
名选手都赛一次。每名选手每天比赛一次,循环赛共进行 n-1 天。要求每天没
有选手轮空 . 以下是八名选手时的循环比赛表,表中第一行为八位选手的编号,
下面七行依次是每位选手每天的对手。
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
【算法分析】
从八位选手的循环比赛表中可以看出 , 这是一个具有对称性的方阵 ,
可以把方阵一分为四来看 , 那么左上角的 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<cstdio>
const int MAXN=33,MAXM=5;
int matchlist[MAXN][MAXN];
int m;
int main()
{
printf("Input m:");
scanf("%d",&m);
int n=1<<m,k=1,half=1; // 1<<m 相当于 2^m
matchlist[0][0]=1;
while (k<=m)
{
for (int i=0;i<half;i++) // 构造右上方方阵
for (int j=0;j<half;j++)
matchlist[i][j+half]=matchlist[i][j]+half;
for (int i=0;i<half;i++) // 对称交换构造下半部分方阵
for (int j=0;j<half;j++)
{
matchlist[i+half][j]=matchlist[i][j+half];
// 左下方方阵等于右上方方阵
matchlist[i+half][j+half]=matchlist[i][j];
// 右下方方阵等于左上方方阵
}
half*=2;
k++;
}
for (int i=0;i<n;i++)
{
for (int j=0;j<n;j++)
printf("%4d",matchlist[i][j]);
putchar('\n');
}
return 0;
}
【例 5 】取余运算( mod )
【问题描述】
输入 b , p , k 的值,求 b^p mod k 的值。
其中 b , p , k*k 为长整形数。
【输入样例】 mod.in
2 10 9
【输出样例】 mod.out
2^10 mod 9=7
【算法分析】
本题主要的难点在于数据规模很大( b , p 都是长
整型数),对于 b^p 显然不能死算,那样的话时间复杂度和
编程复杂度都很大。
下面先介绍一个原理: A*B%K = (A%K )*(B% K )
%K 。显然有了这个原理,就可以把较大的幂分解成较小的,
因而免去高精度计算等复杂过程。那么怎样分解最有效呢?
显然对于任何一个自然数 P ,有 P=2 *
P/2 + P%2 ,如 19=2 * 19/2 + 19%2=2*9+1 ,
利用上述原理就可以把 B 的 19 次方除以 K
的余数转换为求 B 的 9 次方除以 K 的余数,
即 B19=B2*9+1=B*B9*B9 ,再进一步分解下去就
不难求得整个问题的解。
【参考程序】
#include<iostream>
#include<cstdio>
using namespace std;
int b,p,k,a;
int f(int p) // 利用分治求 b^p % k
{
if (p==0) return 1; // b^0 %k=1
int tmp=f(p/2)%k;
tmp=(tmp*tmp) % k; // b^p %k=(b^(p/2))^2 % k
if (p%2==1) tmp=(tmp * b) %k; // 如果 p 为奇数,则 b^p %
return tmp; //k=((b^(p/2))^2)* b%k
}
int main()
{
cin>>b>>p>>k; // 读入 3 个数
int tmpb=b; // 将 b 的值备份
b%=k; // 防止 b 太大
printf("%d^%d mod %d=%d\n",tmpb,p,k,f(p)); // 输出
return 0;
}
【例 8 】、黑白棋子的移动( chessman )
【问题描述】
有 2n 个棋子( n≥4 )排成一行,开始位置为白子全部在左边,黑子全部在
右边,如下图为 n=5 的情形:
○○○○○●●●●●
移动棋子的规则是:每次必须同时移动相邻的两个棋子,颜色不限,可以左
移也可以右移到空位上去,但不能调换两个棋子的左右位置。每次移动必须跳过
若干个棋子(不能平移),要求最后能移成黑白相间的一行棋子。如 n=5 时,成
为:
○●○●○●○●○●
任务:编程打印出移动过程。
【输入样例】 chessman.in
7
【输出样例】 chessman.out
step 0:ooooooo*******--
step 1:oooooo--******o*
step 2:oooooo******--o*
step 3:ooooo--*****o*o*
step 4:ooooo*****--o*o*
step 5:oooo--****o*o*o*
step 6:oooo****--o*o*o*
step 7:ooo--***o*o*o*o*
step 8:ooo*o**--*o*o*o*
step 9:o--*o**oo*o*o*o*
step10:o*o*o*--o*o*o*o*
【算法分析】
我们先从 n=4 开始试试看,初始时:
○○○○●●●●
第 1 步:○○○——●●●○● {— 表示空位 }
第 2 步:○○○●○●●——●
第 3 步:○——●○●●○○●
第 4 步:○●○●○●——○●
第 5 步:——○●○●○●○●
如果 n=5 呢?我们继续尝试,希望看出一些规律,初始
时:
○○○○○●●●●●
第 1 步:○○○○——●●●●○●
第 2 步:○○○○●●●●——○●
这样, n=5 的问题又分解成了 n=4 的情况,下面只要
再做一下 n=4 的 5 个步骤就行了。同理, n=6 的情况又可以分
解成 n=5 的情况,……,所以,对于一个规模为 n 的问题,我
们很容易地就把他分治成了规模为 n-1 的相同类型子问题。
数据结构如下:数组 c[1..max] 用来作为棋子移动的场
所,初始时, c[1]~c[n] 存放白子(用字符 o 表
示), c[n+1]~c[2n] 存放黑子(用字符 * 表
【参考程序】
#include<iostream>
using namespace std;
int n,st,sp;
char c[101];
void print() // 打印
{
int i;
cout<<"step "<<st<<':';
for (i=1;i<=2*n+2;i++) cout<<c[i];
cout<<endl;
st++;
}
void init(int n) // 初始化
{
int i;
st=0;
sp=2*n+1;
for (i=1;i<=n;i++) c[i]='o';
for (i=n+1;i<=2*n;i++) c[i]='*';
c[2*n+1]='-';c[2*n+2]='-';
print();
}
void move(int k) // 移动一步
{
int j;
for (j=0;j<=1;j++)
{
c[sp+j]=c[k+j];
c[k+j]='-';
}
sp=k;
print();
}
void mv(int n) // 主要过程
{
int i,k;
if (n==4) //n 等于 4 的情况要特殊处理
{
move(4); move(8); move(2); move(7); move(1);
}
else
{
move(n); move(2*n-1); mv(n-1);
}
}
int main()
{
cin>>n;
init(n);
mv(n);
【上机练习】
1 、方程 f(x) 的根
【问题描述】 求方程 f(x)=2^x+3^x-4^x=0 在 [1 , 2] 内的根。
提示: 2x 可以表示成 exp(x*log(2)) 的形式。
【输入格式】 输入: [1 , 2] 的区间值。
【输出格式】 输出:方程 f(x)=0 的根, x 的值精确小数点 10 位。
【输入样例】
1 2
【输出样例】
1.5071105957
2 、求一元三次方程的解
【问题描述】
有形如: ax3+bx2+cx+d=0 一元三次方程。给出该方程中各项的系
数 (a , b , c , d 均为实数 ) ,并约定该方程存在三个不同实根
( 根的范围在 -100 至 100 之间 ) ,且根与根之差的绝对值 >=1 。要求
由小到大依次在同一行上输出这三个实根。
【输入格式】 输入: a , b , c , d
【输出格式】 输出: 三个实根(根与根之间留有空格)
【输入样例】 Equ.in
1 –5 –4 20
【输出样例】 Equ.out
-2.00 2.00 5.00
3 、求逆序对
给定一个序列 a1,a2,…,an ,如果存在 i<j 并且 ai>aj ,那么我们称之
为逆序对,求逆序对的数目。
【输入格式】
第一行为 n, 表示序列长度,接下来的 n 行,第 i+1 行表示序列中的
第 i 个数。
【输出格式】
所有逆序对总数。
【输入样例】 deseq. in
4
3
2
3
2
【输出样例】 deseq.out
3
【数据范围】
N<=105 , Ai<=105 。
4 、麦森数( mason )
【问题描述】
形如 2P-1 的素数称为麦森数,这时 P 一定也是个素数。但反过来不一定,即如果 P 是个素数, 2P-
1
不一定也是素数。到 1998 年底,人们已找到了 37 个麦森数。最大的一个是 P=3021377 ,它有
909526 位。麦森数有许多重要应用,它与完全数密切相关。
任务:从文件中输入 P ( 1000<P<3100000 ),计算 2P-1 的位数和最后 500 位数字(用十进制
高精度数表示)。
【输入格式】
文件中只包含一个整数 P ( 1000<P<3100000 )。
【输出格式】
第一行:十进制高精度数 2P-1 的位数;
第 2-11 行:十进制高精度数 2P-1 的最后 500 位数字(每行输出 50 位,共输出 10 行,不足 500
位时高位补 0 );
不必验证 2P-1 与 P 是否为素数。
【输入样例】 mason.in
1279
【输出样例】 mason.out
386
00000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000
00000000000000104079321946643990819252403273640855
38615262247266704805319112350403608059673360298012
23944173232418484242161395428100779138356624832346
49081399066056773207629241295093892203457731833496
61583550472959420547689811211693677147548478866962
50138443826029173234888531116082853841658502825560
46662248318909188018470682222031405210266984354887
32958028878050869736186900714720710555703168729087