说到分治,就不得不先提递归了
递归算法:直接或者间接调用自身的算法称为递归算法。
设计步骤:
- 分析问题、寻找递归关系;
- 设置边界、控制递归;
- 设计函数、确定参数。
接下来看一个递归的例子:递归解决全排列问题
引用问题描述:设R={r1,r2,…,rn}是要进行排列的n个元素,求R的全排列Perm(R)
- 问题分析:
设(ri)perm(X)表示在全排列perm(X)的每一个排列前加上前缀得到的排列。 - 解决方案:
1.递归关系:perm(R)由(r1)perm(R1),(r2)perm(R2) ,…, (rn)perm(Rn)构成,其中Ri=R–{ri}
2.终止条件:n=1时,Perm(R)= r ,r是R中的唯一元素
3.参数设置:待排序数组List,开始下标k,终止下标m - 实现思想:将整组数中的所有的数分别与第一个数交换,这样就总是在处理后n-1个数的全排列。
void Perm(int list[], int k, int m)
{
if(k==m)
{ for(int i=0;i<=m;i++) cout<<list[i];
cout<<endl;
}
else
for(int i=k;i<=m;i++)
{
swap(list[k],list[i]);
Perm(list, k+1,m);
swap(list[k],list[i]);
}
}
- 示例:
当n=3,并且E={a,b,c},对集合E中元素进行全排列,则:
perm(E)=a.perm({b,c}) + b.perm({a,c}) + c.perm({a,b})
perm({b,c})=b.perm(c)+ c.perm(b)
所以,a.perm({b,c})=ab.perm(c)+ ac.perm(b) =ab.c + ac.b=(abc, acb)
同理有b.perm({a,c}) 和c.perm({a,b})
接下来介绍分治算法以及几个例子
- 分治法解决问题的基本思想:
- 将原始问题划分或者归结为规模较小的子问题,这些子问题相互独立且与原问题相同。
- 递归或迭代求解每个子问题。
- 将子问题的解综合得到原问题的解。
- 分治策略注意事项:
- 子问题与原始问题性质完全一样
- 子问题之间可彼此独立地求解
- 递归停止时子问题可直接求解
快速排序
快速排序:对n个元素进行排序。
最好时间复杂度:O(nlogn) 最坏时间复杂度:O(n2)
- 基本步骤:
- 分解:以a[p]为基准元素将a[p:r]分成三段
a[p:q-1], a[q], a[q+1:r]
a[p:q-1]<a[q]<a[q+1:r] - 递归分解:对a[p:q-1]和a[q+1:r]进行排序
- 合并:不需任何运算
void QuickSort(int *a,int p,int r)
{
if(p<r){
int q=Partition(a,p,r);//该函数的功能:把比a[q]小的放在a[q]左边,比a[q]大的a[q]右边。
QuickSort(a,p,q-1);//对左半段排序
QuickSort(a,q+1,r);//对右半段排序
}
}
int Partition(int *a,int p,int r)
{
int i=p,j=r+1;
while(1){
while(a[++i]<=a[p]&&j<r);
while(a[--j]>=a[p]&&j>p);
if(i>=j) break;
swap(a[i],a[j]);
}
swap(a[p],a[j]);
return j;//这里返回的是j的下标,而不是i的下标,因为i要么和j下标一样,要么i在j右边,比j大
}
合并排序
合并排序:一分为二,先给分开的两边分别排序,再合并。
时间复杂度:O(nlogn) 空间复杂度:O(n)
void MergeSort(int a[],int left,int right)
{
int *b=new int [100];
if(left<right)
{ //当数组a中只剩一个元素时,开始往前追溯求解
int i=(left+right)/2;
MergeSort(a,left,i);
MergeSort(a,i+1,right);
Merge(a,b,left,i,right);
Copy(a,b,left,right);
}
}
void Merge(int a[],int b[],int left,int mid,int right)
{ //此过程类似于数据结构刚开始学的两个递增的单链表的合并
int i=left;
int j=mid+1;
int k=left;
while(i<=mid&&j<=right)
{
if(a[i]<a[j]) b[k++]=a[i++];
else b[k++]=a[j++];
}
if(i>mid)
for(int z=j; z<=right; z++)
b[k++]=a[z];
else
for(int z=i; i<=mid; i++)
b[k++]=a[z];
}
void Copy(int a[],int b[],int left,int right)
{ //数组b赋值给数组a
for(int i=left; i<=right; i++)
a[i]=b[i];
}
棋盘覆盖问题
问题描述:
在一个2k*2k 个方格组成的棋盘中,恰有一个方格与其它方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。
在棋盘覆盖问题中,要用4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且y要求任何2个L型骨牌不得重叠覆盖。
- 代码分析:
当k>0时,将2k×2k棋盘分割为4个(2k-1)×(2k-1) 子棋盘。特殊方格必位于这4个较小子棋盘之一中,其余3个子棋盘中无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘,可以用一个L型骨牌覆盖这3个较小棋盘的会合处,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为棋盘1×1。
void chessBoard(int tr, int tc, int dr, int dc, int size)
{ //特殊方格横坐标dr,纵坐标dc;当前棋盘横坐标tr,纵坐标tc
if (size == 1) return;
int t = tile++;// L型骨牌号,用数字代替L骨牌
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 < tc + s)
chessBoard(tr+s, tc, dr, dc, s);
else {
board[tr + s][tc + s - 1] = t;
chessBoard(tr+s, tc, tr+s, tc+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);
}//右下方
}
时间复杂度:O(4k)
循环赛日程表
问题描述:
设有n=2k个运动员要进行循环赛,设计一个比赛日程表,要求满足以下条件的:
1.每个选手必须与其他n-1个选手各赛一次;
2.每个选手一天只能赛一次;
3.循环赛一共进行n-1天。
-
分治策略:
可以通过为n/2个选手设计的比赛日程表来决定。递归地对选手进行分割,直到只剩下2个选手时,比赛日程表的制定就变得很简单。这时只要让这2个选手进行比赛就可以了。
该表中,第一列的1 2 3…表示第i个人的比赛安排,第二列之后直到第n列(用第k列表示)指第k-1天第i个人的比赛安排。
void Table(int k,int **a)
{
int n=1;
for(int i=1; i<=k; i++) n*=2;
for(int i=1; i<=n; i++) a[1][i]=i;
int m=1; //初始化第一行
for(int s=1; s<=k; s++)
{ //由于每次安排人数都必须是2^k,所以只需循环k遍。
n/=2;
for(int t=1; t<=n; t++){//对当前表格进行安排
for(int i=m+1; i<=2*m; i++){
for(int 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;
}
}
}
}
接下来看一个简单题:用递归与分治策略求n个元素中的最大值。
求n个元素中的最大元素值,要求用递归与分治策略解决。
输入
第1行:8 元素个数
第2行:10 3 9 20 4 83 24 65 8个元素的值
输出: 83 最大元素值
#include <bits/stdc++.h>
using namespace std;
int isMax(int *a, int begin,int end)
{
if (begin == end){
return a[end];
}
int mid=(begin + end)/2;
int x=isMax(a, begin, mid);
int y=isMax(a, mid + 1, end);
return max(x,y);
}
int main()
{
int n;
cin >> n;
int a[100];
for(int i=0; i<n;i++) cin >> a[i];
cout << isMax(a,0,n-1);
}