【算法学习】实验 2. 基于分治法的循环日程表算法
实验内容
本实验要求基于算法设计与分析的一般过程(即待求解问题的描述、算法设计、算法
描述、算法正确性证明、算法分析、算法实现与测试),针对循环日程表算法从实践中理解
分治法的思想、求解策略及步骤。
实验目的
- 理解分治法的核心思想以及分治法求解过程
- 从算法分析与设计的角度,对快速排序算法有更进一步的理解。
实验要求
对于环境没有特别要求。对于算法实现,可以自由选择 C, C++, Java,甚至于其他程序设计语言。
实验步骤
步骤一:理解问题并给出问题描述
设有 \(n=2^k\) 个运动员要进行网球循环赛。现要设计一个满足以下要求的比赛日程表:
(1) 每个选手必须与其他 \(n-1\)个选手各赛一次;
(2) 每个选手一天只能参赛一次;
(3) 循环赛在$ n-1$天内结束。
请按此要求将比赛日程表设计成有n行和n-1列的一个表。在表中的第i行,第j列处填入第i个选手在第j天所遇到的选手。其中 $ 1≤i≤n $ ,\(1≤j≤n-1\)。
步骤二:算法设计,包括策略与数据结构
按分治策略,我们可以将所有的选手分为两半,则n个选手的比赛日程表可以通过n/2个选手的比赛日程表来决定。递归地用这种一分为二的策略对选手进行划分,直到只剩下两个选手时,比赛日程表的制定就变得很简单。这时只要让这两个选手进行比赛就可以了。如上图,所列出的正方形表是8个选手的比赛日程表。其中左上角与左下角的两小块分别为选手1至选手4和选手5至选手8前3天的比赛日程。据此,将左上角小块中的所有数字按其相对位置抄到右下角,又将左下角小块中的所有数字按其相对位置抄到右上角,这样我们就分别安排好了选手1至选手4和选手5至选手8在后4天的比赛日程。依此思想容易将这个比赛日程表推广到具有任意多个选手的情形。
算法实现部分打算采用递归和非递归两种方式,数据结构主要有二维数组和栈。
步骤三:描述算法,伪代码与流程图的形式
1. 递归形式的伪代码:
ROUND-ROBIN-CALENDAR-REC(A,n):
if n == 1:
then A[1][1] = 1
return
ROUND-ROBIN-CALENDAR-REC(A,n/2)
m=n/2
for i = 1 to m
for j = 1 to m
do
A[i][j+m]=A[i][j]+m
A[i+m][j]=A[i][j+m]
A[i+m][j+m]=A[i][j]
描述算法:
该分治算法可以看做左上角矩阵块跟右下角的矩阵块一致,左下角的矩阵块与右上角的矩阵块一致。如下图:
input k:3
[[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.]]
左上角与右上角的关系就是加上子矩阵的长度的值,这样就可以通过递归进行求解:
递归的退出条件为:矩阵的长度为1, 这时候给最左上角的单元格赋值为1
递归过程:
- 如果不满足递归退出条件,那就分治,将矩阵块切割为原来的一半进行继续递归
- 否则进行赋值的处理,
- 右上角的值 = 左上角的值 + 矩阵长度的一半
- 右下角的值 = 左上角的值
- 左下角的值 = 右上角的值
2. 非递归形式的伪代码:
ROUND-ROBIN-CALENDAR(K,A)
m=1
n=2^k
for i in 1 to n:
A[1][i]=i
C=1 //位置计数
for s in 1 to k:
n/=2
for t in 1 to n:
for i in m+1 to 2*m:
for j in m+1 to 2*m:
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
描述算法
先对第一行进行初始化,然后通过迭代的方式进行二维数组的填充。
每次进行两个方向上的填充:
- 左上角 = 右下角
- 左下角 = 右上角
步骤四:算法正确性证明
分治算法求解主要遵循三大步骤,分解、解决、合并
分解:将原问题划分成形式相同的子问题,规模可以不等,对半或2/3对1/3的划分。
解决:对于子问题的解决,很明显,采用的是递归求解的方式,如果子问题足够小了,就停止递归,直接求解。
合并:将子问题的解合并成原问题的解。
非递归算法的正确性比较容易解释,通过日程表找到的规律,我们通过迭代的方式进行实现,这样得到的结果一定是正确的。
递归算法正确性从三个步骤来讲:
- 分解:我们将日程表进行分解,每次分解为原来的一半,直到仅有一个单元格为止
- 解决:递归结束的地方一定是表的左上角,赋值为1,反向合并的时候可以继续通过左上角=右下角,左下角=右上角,右上角 =左上角+n/2得到
- 合并:将每个自问题的节合并为元问题的节,通过三个复制,可以得到其他三部分的日程表数据
这样处理能确保没有重复和一类,所以该算法也是正确的。
步骤五:算法复杂性分析
1. 递归算法
1.1 递归算法的时间复杂度分析:
1.2 递归算法的空间复杂度分析:
主要是在栈所占用的空间,由于是分治的方法,所以空间复杂度为:$ O(logn) $
2. 非递归算法
2.1 非递归算法的复杂度分析
2.2 非递归算法的空间复杂度分析
整个过程中借助几个变量的空间,所以空间复杂度为\(O(1)\)
步骤六:算法的实现与测试,附上代码及运行结果
C++版本的算法实现
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e4;
void output(int n,int **mp,int t);
void fillMp(int k,int n,int **mp);
int main()
{
int k;
cin >> k;
//开辟空间
int **mp = new int*[maxn];
for(int i = 0 ; i < maxn ; i++)
mp[i] = new int[maxn];
for(int i=0; i< maxn ;i++){
for(int j = 0 ;j < maxn ;j++){
mp[i][j]=0;
}
}
int n = pow(2,k);
cout << n << endl;
fillMp(k,n,mp);
output(n,mp,-1);
//释放空间
for(int i = 0 ; i < maxn ; i++)
delete[] mp[i];
delete[] mp;
return 0;
}
//k现在是多少选手参加比赛,n为现在的矩阵维度
void fillMp(int k,int n,int **mp)
{
int m = 1;
for(int i = 0 ; i < n ; i++){
mp[0][i] = i+1;
}
output(n,mp,-2);
//二分k次
for(int s = 0 ; s < k ; s++)
{
n/=2;
//针对每一个格子进行分析的,所以会有n个
for(int t = 0 ; t < n ; t++)
{
for(int i= m; i < 2*m ; i++)
{
for(int j = m ;j < 2*m ; j++)
{
//右下角=左上角
mp[i][j+t*m*2]=mp[i-m][j+t*m*2-m];
mp[i][j+t*m*2-m]=mp[i-m][j+t*m*2];
}
}
output(15,mp,t);
}
m *= 2;
}
}
void output(int n,int **mp,int t){
cout << "output:" << t << endl;
for(int i = 0 ; i < n ; i++){
for(int j = 0 ;j < n ; j++){
cout << mp[i][j] << '\t';
}
cout << endl;
}
return;
}
C++版本结果(非递归实现):
3
8
output:-2
1 2 3 4 5 6 7 8
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
output:0
1 2 3 4 5 6 7 8
2 1 4 3 6 5 8 7
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
output: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
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
output: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
Python版本的算法实现(递归实现):
# -*- coding: utf-8 -*-
"""
Created on Tue Oct 2 22:11:51 2018
@author: pprp
"""
import numpy as np
def init(a,d):
for i in range(d):
a[0,i] = i+1
def solveTable(n,a):
Hf = int(len(a)/2)
if n == 1:
a[0][0] = 1
return
solveTable(int(n/2),a[0:Hf,0:Hf])
for i in range(Hf):
for j in range(Hf):
a[i,j+Hf]=a[i,j]+Hf
a[i+Hf][j]=a[i][j+Hf]
a[i+Hf][j+Hf]=a[i][j]
if __name__ == "__main__":
#k = input("input k:")
k = 3
d = 2**int(k)
a = np.zeros((d,d))
init(a,d)
solveTable(d,a)
print(a)
python版本结果:
input k: 3
[[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.]]
实验总结
分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
分治法所能解决的问题一般具有以下几个特征:
1、该问题的规模缩小到一定的程度就可以容易地解决
2、该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3、利用该问题分解出的子问题的解可以合并为该问题的解;
4、该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
分治法在每一层递归上都有三个步骤:
1、分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
2、解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
3、合并:将各个子问题的解合并为原问题的解。
总结:
分治算法是一种思想,将问题分而治之,通常需要借助递归的帮助。关键点在于如何将问题分解,不仅仅将问题规模减半就可以,还需要仔细思考如何将分解的问题进行合并。需要注意分治算法分解的时候,出来的自问题通常是与原问题性质相同,但规模小的子问题。