目录
一.分治思想
1.基本思想
分治法求解问题的过程是,将整个问题分解成若干个小问题后分而治之。如果分解得到的子问题相对来说还太大,则可反复使用分治策略将这些子问题分成更小的同类型子问题,直至产生出方便求解的子问题,必要时逐步合并这些子问题的解,从而得到问题的解。
2.基本步骤
1)分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
2)解决:若子问题规模较小而容易被解决则直接解,否则再继续分解为更小的子问题,直到容易解决;
3)合并:将已求解的各个子问题的解,逐步合并为原问题的解。
3.分治法的应用
当求解一个输入规模为n且取值又相当大的问题时,用枚举算法效率一般得不到保证。若问题能满足以下几个条件,就能用分治法来提高解决问题的效率。
1) 该问题的规模缩小到一定的程度就可以容易地解决;
2) 该问题可以分解为若干个较小规模的相同问题;
3)利用该问题分解出的子问题的解可以合并为该问题的解。
4)该问题所分解出的各个子问题都是相互独立的,即子问题之间不包含公共的子子问题。
4.分治思想基本代码
divide-and-conquer(int n)//n为待解决问题的规模
{
if(n<=n0)//n0为可解子问题的规模
{
解决子问题;
return(子问题的解);
}
否则就分解成若干小问题
for(int i=1;i<=k;i++)//分解成p1,...,pk这些个小问题进行解决
yi=divide-and-conquer(|pi|);//递归解决pi
T=merge(y1,...,yk);//合并子问题
return(T);
}
二.分治示例
1.查找有序数组中指定数的位置
二分查找,复杂度为O(logn)
#include<bits/stdc++.h>
using namespace std;
int bisearch(int a[],int x,int n)
{
int left=0,right=n-1;
while(left<=right)
{
int mid=(left+right)/2;
if(x==a[mid])
return mid;
else if(x>a[mid])
left=mid+1;
else
right=mid-1;
}
return -1;
}
int main()
{
int a[]={1,3,4,5,6,55,66};
int x;
while(cin>>x)
{
cout<<"The pos of x is:\n";
cout<<bisearch(a,x,7)<<endl;
}
return 0;
}
2.求x的n次方
1)莽一把: 用循环求X^n=X*X*X*......X
2)分治法
分成奇偶两个小策略,出口是n=1的时候
#include<bits/stdc++.h>
using namespace std;
int fun(int x,int n)
{
//递归出口
if(n==1)
return x;
else if(n%2==0)
return fun(x,n/2)*fun(x,n/2);
else
return fun(x,(n-1)/2)*x;
}
int main()
{
int x,n;
while(cin>>x>>n)
{
cout<<fun(x,n)<<endl;
}
return 0;
}
3.大整数乘法
设X, Y是n位二进制整数,分段表示如下:
即 X=A2n/2+B, Y=C2n/2+D 则
XY=(A2^n/2+B)(C2^n/2+D)=AC2^n+(AD+BC) 2^n/2+BD
计算成本:4次n/2位乘法,3次不超过n位加法,2次移位,所有加法和移位共计O(n)次运算。
复杂度为O(n^2)
由X=A2n/2+B, Y=C2n/2+D 则
XY=(A2n/2+B)(C2n/2+D)=AC2n+(AD+BC) 2n/2+BD= AC2^n+((A-B) (D-C)+AC+BD)2^n/2+BD
计算成本:3次n/2位乘法,6次不超过n位加减法,2次移位,所有加法和移位共计O(n)次运算。由此可得
复杂度为O(n^1.59)
举个栗子
已知X=2368,y=3925,求XY
解:取基d=10, A=23, B=68, C=39, D=25,(d=10是指十进制的乘法,可以换成别的进制进行计算,试过了,可以用)
① AC=23×39=897
② BD=68×25=1700
③ (A-B)(D-C)+AC+BD
=(23-68)(25-39)+897+1700
=45×14+897+1700=3227
XY=1700+3227×102+897×104=9294400
4.金块问题
老板有一袋金块(共n块),最优秀的雇员得到其中最重的一块,最差的雇员得到其中最轻的一块。假设有一台比较重量的仪器,我们希望用最少的比较次数找出最重的金块。
1)莽
比较简单的方法是逐个的进行比较查找。先拿两块比较重量,留下重的一个与下一块比较,直到全部比较完毕,就找到了最重的金子。算法类似于一趟选择排序。
maxmin(float a[],int n)
{
int i,max,min;
max = a[1];
min = a[1];
for(i = 2;i < n;i++)
if(a[i] >max)
max = a[i];
else
min = a[i];
}
算法分析:最好情况(从小到大取出,不需要和min比较)比较n-1次,最坏情况比较2(n-1)次,平均比较为3(n-1)/2
2)分治
问题可以简化为:在含n(n是2的幂(n>=2))个元素的集合中寻找极大元和极小元。用分治法(二分法)可以用较少比较次数地解决上述问题:
1) 将数据等分为两组(两组数据可能差1),目的是分别选取,其中的最大(小)值。
2) 递归分解直到每组元素的个数≤2,可简单地找到最大(小) 值。
3) 回溯时将分解的两组解大者取大,小者取小,合并为当前问题的解。
#include<bits/stdc++.h>
using namespace std;
#define N 8
float a[N] = {4.5,2.3,3.4,1.2,6.7,9.8,5,6};
void maxmin(float &fmax, float &fmin, int i, int j)
{
int mid;
float lmax, lmin, rmax, rmin;
if (i == j)
{
fmax = a[i];
fmin = a[i];
}
else if(i==j-1)//只剩下最后两个
{
if (a[i] < a[j])
{
fmax = a[j];
fmin = a[i];
}
else
{
fmax = a[i];
fmin = a[j];
}
}
else
{
mid = (i + j) / 2;
maxmin(lmax, lmin,i, mid);
maxmin(rmax, rmin,mid + 1, j);
fmax=lmax>rmax?lmax:rmax;
fmin=lmin<rmin?lmin:rmin;
}
}
int main()
{
float max, min;
maxmin(max,min,0,N-1);
printf("Max=%.1f\nMin=%.1f\n",max,min);
return 0;
}
5.循环赛日程表
问题描述
设计一个满足以下要求的比赛日程表(2^k个选手) :
(1)每个选手必须与其他n-1个选手各赛一次;
(2)每个选手一天只能赛一次;
(3)循环赛一共进行n-1天。
问题分析:
将所有的选手分为两半,n个选手的比赛日程表就可以通过为n/2个选手设计的比赛日程表来决定。递归地用对选手进行分割,直到只剩下2个选手时,比赛日程表的制定就变得很简单。这时只要让这2个选手进行比赛就可以了。
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 |
void ContestTable(int k, int **a)
{ int i,j,s,t,n=1,m=1;
for(i=1;i<=k;i++) n*=2;
for(i=1;i<=n;i++) a[1][i]=i;
for(s=1;s<=k;s++)
{ n/=2;
for(t=1;t<=n;t++)
for(i=m+1;i<=2*m;i++)
for(j=m+1;j<=2*m;j++)
{
a[i][j+(t-1)*m*2]=a[i-m][j+(t-1)*m*2-m];//左上赋给右下
a[i][j+(t-1)*m*2-m]=a[i-m][j+(t-1)*m*2];//右上赋给左下
}
m*=2;
}
}
//转载自qinyg
#include<iostream>
using namespace std;
int a[100][100];
int n; //选手的个数
/*
tox:目标数组的行号
toy:目标数组的列号
fromx:源数组的行号
fromy:源数组的列号
r:数组的大小为 r*r
*/
void Copy(int tox, int toy, int fromx, int fromy, int r)
{
for(int i = 0; i < r; i++)
for(int j = 0; j < r; j++)
a[tox+i][toy+j] = a[fromx+i][fromy+j];
}
void Table(int k)
{
n = 1 << k;
//构造正方形表格的第一行数据
for(int i = 0; i < n; i++)
a[0][i] = i + 1;
//采用分治算法,构造整个循环赛日程表
for(int r = 1; r < n; r <<= 1)
for(int i = 0; i < n; i += 2*r)
{
Copy(r, r + i, 0, i, r); //左上角复制到右下角
Copy(r, i, 0, r + i, r); //右上角复制到左下角
}
}
int main()
{
int k;
cout<<"请输入k的值:";
cin>>k;
Table(k);
for(int i = 0; i < n; i++)
{
for(int j = 0; j < n; j++)
{
cout<< a[i][j] << " ";
}
cout<<endl;
}
return 0;
}
6.棋盘覆盖
问题描述
在一个 2k * 2k 个方格组成的棋盘中,有一个方格与其它的不同,若使用以下四种 L 型骨牌覆盖除这个特殊方格的其它方格,如何覆盖。四个 L 型骨牌如下图:
图1.1 L型骨牌
棋盘中的特殊方格如图:
图1.2 存在特殊方格的棋盘
覆盖完成后的棋盘:
#include<bits/stdc++.h>
using namespace std;
int board[100][100];
int tile=0;
//tr 棋盘中左上角方格所在行
//tc 棋盘中左上角方格所在列
//dr 残缺方格所在行
//dc 残缺方格所在列
void chessboard(int tr,int tc,int dr,int dc,int size)
{
if(size==1)
return ;
int t=tile++;//L型骨牌号
int s=size/2;
if(dr<tr+s&&dc<tc+s)
chessboard(tr,tc,dr,dc,s);
else
{
board[tr+s-1][tc+s-1]=t;
chessboard(tr,tc,tr+s-1,tc+s-1,s);
}
if(dr<tr+s&&dc>=tc+s)
chessboard(tr,tc+s,dr,dc,s);
else
{
board[tr+s-1][tc+s]=t;
chessboard(tr,tc+s,tr+s-1,tc+s,s);
}
if(dr>=tr+s&&dc<tr+s)
chessboard(tr+s,tc,dr,dc,s);
else
{
board[tr+s][tc+s-1]=t;
chessboard(tr+s,tc,tr+s,tr+s-1,s);
}
if(dr>=tr+s&&dc>=tc+s)
chessboard(tr+s,tc+s,dr,dc,s);
else
{
board[tr+s][tc+s]=t;
chessboard(tr+s,tc+s,tr+s,tc+s,s);
}
}
//以棋盘规格为4*4为例
int main()
{
chessboard(0 , 0 , 1 , 3 , 4);
//输出覆盖完成后的棋盘
for(int i = 0 ; i < 4; i++)
{
for(int j = 0 ; j < 4; j++)
{
cout<<board[i][j]<<" ";
}
cout<<endl;
}
return 0;
}
7.普通矩阵乘法
问题描述
设n×n阶矩阵A和B,计算C=A×B
???=?=1????????,?=1,2,…,?C_i^j=∑_(k=1)^n▒〖A_ik B_kj 〗 i,j=1,2,…,n
求C=AB即对n2个元素cij进行计算,故要作n3次乘法。相当长时间内没有人怀疑过是否可以用少于n3次乘法来完成。
时间复杂度分析
用了7次对于n/2阶矩阵乘积的递归调用和18次n/2阶矩阵的加减运算。由此可知,该算法的所需的计算时间T(n)满足如下的递归方程:
T(n)=O(nlog7)=O(n2.81)
8.数列的最大子段和(子类重叠问题,简单的二分无法解决)
给定n个元素的整数列(可以能为负整数),a1,a2,…,an。求数列的字段,使其和最大。
例如:当(a1, a2, a3, a4, a5, a6)=(-2, 11, -4, 13, -5, -2)时,最大子段和为sum(11-4+13)=20。
算法设计:此问题用分治法分解后的两个子序列(子问题)并不独立,因为有可能最长的连续不升数列正好存在于两个子序列的连接位置。
如果将所给的序列a[1..n]分为长度相等的2段a[1--(n/2)]和a[(n/2)+1--n],则a[1--n]的最长连续不升数列有3种情况:
1) a[1..n]的最长连续不升数列与a[1..(n/2)]的最长连续不升数列相同;
2) a[1..n]的最长连续不升数列与a[(n/2)+1..n]的最长连续不升数列相同;
3) a[1..n]的最长连续不升数列为∑a[k],且1≤i≤(n/2), (n/2)+1≤j≤n。
情况1)和情况2)可分别递归求得。
对于情况3),a[(n/2)]与a[(n/2)+1]一定在最优子序列中。因此,我们可以计算出a[i..(n/2)]的最大值s1;并计算出a[(n/2)+1..j]中的最大值s2。则s1+s2即为出现情况3)时的最优值。
#include<bits/stdc++.h>
using namespace std;
int maxsub(int a[],int left,int right)
{
int middle,i,j,left_sum,right_sum,s1,s2,lefts,rights;
if(left==right)
if(a[left]>0)
return a[left];
else
return 0;
else
{
middle=(left+right)/2;
left_sum=maxsub(a,left,middle);
right_sum=maxsub(a,middle+1,right);
s1=0;
lefts=0;
for(i=middle;i>=left;i--)
{
lefts+=a[i];
if(lefts>s1)
s1=lefts;
}
s2=0;
rights=0;
for(i=middle+1;i<=right;i++)
{
rights+=a[i];
if(rights>s1)
s2=rights;
}
if(s1+s2<left_sum&&right_sum<left_sum)
return left_sum;
if(s1+s2<right_sum)
return right_sum;
return s1+s2;
}
}
int main()
{
int aa[10];
int n;
cout<<"请问输入几个数?\n";
cin>>n;
for(int i=0;i<n;i++)
cin>>aa[i];
cout<<"最大子段和为\n";
cout<<maxsub(aa,0,n-1)<<endl;
return 0;
}
8.求一组数中第二小的数
问题描述
如题
算法设计
在用二等分法分解的两个子集中,无论只选取第二小数或只选取最小的数,合并处理后都有可能得到原问题的正确解。但若在两个子集中都选取最小的两个值,那么,原问题中第二小的数则一定在这4个数之中。
将问题转化为“求一组数中较小的两个数”后,二等分法分解后就可将原问题“分解为与原问题独立且相似的两个子问题”了。
#include<bits/stdc++.h>
using namespace std;
int a[100];
two(int i,int j,int &fmin2,int &fmin1)
{
int lmin1,rmin1,lmin2,rmin2;
int mid;
if(i==j)
fmin2=fmin1=a[i];
else if(i==j-1)//就剩下两个数的时候即为出口
{
if(a[i]<a[j])
{
fmin1=a[i];
fmin2=a[j];
}
else
{
fmin1=a[j];
fmin2=a[i];
}
}
else
{
mid=(i+j)/2;
two(i,mid,lmin2,lmin1);
two(mid+1,j,rmin2,rmin1);
if(lmin1<rmin1)
{
if(lmin2<rmin1)
{
fmin1=lmin1;
fmin2=lmin2;
}
else
{
fmin1=lmin1;
fmin2=rmin1;
}
}
else
{
if(rmin2<lmin1)
{
fmin1=rmin1;
fmin2=rmin2;
}
else
{
fmin1=rmin1;
fmin2=lmin1;
}
}
}
}
int second(int n)
{
int min2,min1;
two(0,n-1,min2,min1);
return min2;
}
int main()
{
int n;
int min2;
cout<<"请问要输入多少个数?\n";
cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
min2=second(n);
cout<<min2<<endl;
return 0;
}
9.第k小的数字(改写快排)
问题描述
如题
算法设计
本题可以对全部数据进行排序后,找出问题的解。用较好的排序方法,算法的复杂性为O( nlogn)
可以通过改写快速排序算法来解决选择问题,一趟排序分解出的左子集中元素个数nleft,可能是以下几种情况:
1) nleft=k-1,则分界数据就是选择问题的答案。
2) nleft>k-1,则选择问题的答案继续在左子集中找,问题规模变小了。
3) nleft<k-1,则选择问题的答案继续在右子集中找,问题变为选择第k-nleft-1小的数,问题的规模也变小了
#include<bits/stdc++.h>
using namespace std;
int a[100];
int select(int a[ ], int left, int right, int k)
{
if (left >= right)
return a[left];
int i = left,j,t; //从左至右的指针
j = right + 1; // 从右到左的指针
int pivot = a[left]; //把最左面的元素作为分界数据
while (1)
{
do
{
i = i + 1; // 在左侧寻找>= pivot 的元素
} while (a[i] < pivot);
do
{
j = j - 1;
} while (a[j] > pivot); // 在右侧寻找<= pivot 的元素
if (i >= j) break; // 未发现交换对象
t=a[i];
a[j]=a[i];
a[i]=t;
}
if (j - left + 1 == k)
return pivot;
a[left] = a[j]; // 设置p i v o t
a[j] = pivot;
if (j - left + 1 < k) // 对一个段进行递归调用
return select(a, j+1, right, k-j -1+left);
else
return select(a, left, j-1, k);
}
int xzwt(int a[ ], int n, int k ) //返回a [ 0 : n - 1 ]中第k小的元素
{
if (k < 1 || k > n)
printf("ERROR\n");
return select(a, 0, n-1, k);
}
main()
{
int i,n,k;
scanf("%d%d",&n,&k);
for(i=0;i<n;i++)
scanf("%d",&a[i]);
printf("%d \n",xzwt(a,n,k));
}
10.快排算法思想
取序列的一个元素作为轴,利用这个轴把序列分成三段:左段,中段(轴), 和右段, 使左段中各元素都小于等于轴,右段中各元素都大于等于轴。(这个过程称做对序列的划分)
左段和右段的元素可以独立排序, 将排好序的三段合并到一起即可
上面的过程可以递归地执行,直到每段的长度为1
最坏运行时间: Q(n2)
平均运行时间: Q(nlogn)
三.练习
1.伪币问题
问题描述
假设一个袋子中装有16枚硬币。16枚硬币中有且仅有一枚是伪造的,并且那枚伪币要较真的硬币轻些。现在考虑如何找出这个伪造的硬币,这里仅提供一台可用于比较两组硬币重量的天平。通过这台仪器,可以知道两组硬币的重量是否相同。
问题分析
方法一:先取一枚币a,再接着取剩下的币和a进行称量,
直到找到一个币和a再天平上不平衡,哪一边轻哪一边就是伪币
方法二:16个币先分成2堆8个的,把两堆进行称量比较,
伪币在轻的那一堆中,以此类推,直到最后剩下两个币就可以找出伪币
//测试样例
/*
1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1
*/
//方法一:先取一枚币a,再接着取剩下的币和a进行称量,
//直到找到一个币和a再天平上不平衡,哪一边轻哪一边就是伪币
#include<iostream>
using namespace std;
int main()
{
int weight[17];
cout<<"Please input 16 weight:\n";
for(int i=0;i<15;i++)
cin>>weight[i];
int fake=weight[0],ifake;
for(int i=0;i<15;i++)
{
if(weight[i]<=fake)
{
fake=weight[i];
ifake=i;
}
}
cout<<"The position of the fake coin is:\n";
cout<<ifake+1<<endl;
return 0;
}
//方法二:16个币先分成2堆8个的,把两堆进行称量比较,
//伪币在轻的那一堆中,以此类推,直到最后剩下两个币就可以找出伪币
#include<iostream>
using namespace std;
int find_coin(int weight[],int low,int high)
{
int sum1=0,sum2=0;
if(high-low==1)
{
if(weight[low]>weight[high])
return high;
else return low;
}
if((high-low+1)%2==0)
{
for(int i=low;i<low+(high-low+1)/2;i++)
sum1+=weight[i];
for(int i=(high-low+1)/2;i<=high;i++)
sum2+=weight[i];
if(sum1<sum2)
find_coin(weight,low,low+(high-low)/2);
else
find_coin(weight,low+(high-low+1)/2,high);
}
else if((high-low)%2==0)
{
for(int i=low;i<(high-low)/2;i++)
sum1+=weight[i];
for(int i=(high-low)/2+1;i<=high;i++)
sum2+=weight[i];
if(sum1<sum2)
find_coin(weight,low,(high-low)/2-1);
else if(sum1>sum2)
find_coin(weight,1+(high-low)/2,high);
else
return (high-low)/2;
}
}
int main()
{
int weight[17];
cout<<"Please input 16 weight:\n";
for(int i=0;i<16;i++)
cin>>weight[i];
cout<<"The position of the fake coin is:\n";
cout<<find_coin(weight,0,15+1)+1<<endl;
return 0;
}
2.集合划分
问题描述
给定正整数n和m,利用分治算法计算出n个元素的结合{1,2,...,n}可以划分为多少个不通过的由m个非空子集组成的集合。
Input:元素个数n和非空子集数
Output:计算出共有多少个不同的由m个非空子集组成的集合
问题分析
写出几个特例得出递推式:fun(m,n)=fun(m-1,n-1)+m*fun(m,n-1)
#include<iostream>
using namespace std;
int fun(int m,int n)
{
if(m==1||m==n)
return 1;
else
return fun(m-1,n-1)+m*fun(m,n-1);
}
int main()
{
int n,count=0;
while(cin>>n)
{
for(int i=1;i<=n;i++)
count+=fun(i,n);
cout<<count<<endl;
count=0;
}
return 0;
}
3.整数因子分解
问题描述
大于1 的正整数n 可以分解为:n=x1 *x 2*…*xm 。
例如,当n= 12 时,共有8 种不同的分解式:
12= 12;
12=6*2;
12=4*3;
12=3*4;
12=3*2*2;
12=2*6;
12=2*3*2;
12=2*2*3。
编程任务:
对于给定的正整数n,编程计算n 共有多少种不同的分解式。
数据输入:
输入数据第一行有1 个正整数n (1≤n≤2000000000) 。
结果输出:
将计算出的不同的分解式数。
输入 输出
12 8
#include <stdio.h>
#include <stdlib.h>
int count;
void solve(int n)
{
int i;
if(n == 1)//当商为1时即为已经算出一次分解累计+1
count++;
for(i=2;i<=n;i++)//每个数进行遍历
{
if(n % i == 0)//mod为0 即为可分解的数
solve(n/i);//进行分解
}
}
int main()
{
int n;
while(scanf("%d",&n)!=EOF)
{
count = 0;
solve(n);
printf("%d\n",count);
}
return 0;
}