【算法学习】实验 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

递归过程:

  • 如果不满足递归退出条件,那就分治,将矩阵块切割为原来的一半进行继续递归
  • 否则进行赋值的处理,
  1. 右上角的值 = 左上角的值 + 矩阵长度的一半
  2. 右下角的值 = 左上角的值
  3. 左下角的值 = 右上角的值

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

描述算法

    先对第一行进行初始化,然后通过迭代的方式进行二维数组的填充。

    每次进行两个方向上的填充:

  1. 左上角 = 右下角
  2. 左下角 = 右上角

步骤四:算法正确性证明

    分治算法求解主要遵循三大步骤,分解、解决、合并

分解:将原问题划分成形式相同的子问题,规模可以不等,对半或2/3对1/3的划分。

解决:对于子问题的解决,很明显,采用的是递归求解的方式,如果子问题足够小了,就停止递归,直接求解。

合并:将子问题的解合并成原问题的解。

    非递归算法的正确性比较容易解释,通过日程表找到的规律,我们通过迭代的方式进行实现,这样得到的结果一定是正确的。

递归算法正确性从三个步骤来讲:

  1. 分解:我们将日程表进行分解,每次分解为原来的一半,直到仅有一个单元格为止
  2. 解决:递归结束的地方一定是表的左上角,赋值为1,反向合并的时候可以继续通过左上角=右下角,左下角=右上角,右上角 =左上角+n/2得到
  3. 合并:将每个自问题的节合并为元问题的节,通过三个复制,可以得到其他三部分的日程表数据

    这样处理能确保没有重复和一类,所以该算法也是正确的。

步骤五:算法复杂性分析

1. 递归算法

1.1 递归算法的时间复杂度分析:

[T(n) = T(n/2)+O(n/2)^2 ]

[T(n) = T(n/4)+O(n/4)^2+O(n/2)^2 ]

[T(n) = 1 + O(1)^2 + O(2)^2 + ... + O(n/4)^2+O(n/2)^2 = O(n^2) ]

1.2 递归算法的空间复杂度分析:

主要是在栈所占用的空间,由于是分治的方法,所以空间复杂度为:$ O(logn) $

2. 非递归算法

2.1 非递归算法的复杂度分析

[T(n) = O(frac{n}{2}*1^2 + frac{n}{4}*2^2 + frac{n}{8}*4^2 + ... + frac{n}{2^k}*(2^{k-1})^2) ]

[T(n) = O(n(2^{-1}+2^0+2^1+...+2^{k-2})) = O(n^2) ]

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] << '	';
        }
        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、合并:将各个子问题的解合并为原问题的解。

总结:

    分治算法是一种思想,将问题分而治之,通常需要借助递归的帮助。关键点在于如何将问题分解,不仅仅将问题规模减半就可以,还需要仔细思考如何将分解的问题进行合并。需要注意分治算法分解的时候,出来的自问题通常是与原问题性质相同,但规模小的子问题。

原文地址:https://www.cnblogs.com/pprp/p/9880042.html