分治

目录

 

一.分治思想

1.基本思想

2.基本步骤

3.分治法的应用

4.分治思想基本代码

二.分治示例

1.查找有序数组中指定数的位置

2.求x的n次方

3.大整数乘法

4.金块问题

5.循环赛日程表

6.棋盘覆盖

7.普通矩阵乘法

8.数列的最大子段和(子类重叠问题,简单的二分无法解决)

8.求一组数中第二小的数

9.第k小的数字(改写快排

10.快排算法思想

三.练习

1.伪币问题

2.集合划分


一.分治思想

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, Yn位二进制整数,分段表示如下:

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

计算成本:4n/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

计算成本3n/2位乘法,6次不超过n位加减法,2次移位,所有加法和移位共计O(n)次运算。由此可得

复杂度为O(n^1.59)

举个栗子

已知X=2368y=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×102897×1049294400

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)分治

问题可以简化为:在含nn2的幂(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阶矩阵AB,计算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阶矩阵乘积的递归调用和18n/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]分为长度相等的2a[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;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值