zoukankan      html  css  js  c++  java
  • 【算法学习】实验 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、合并:将各个子问题的解合并为原问题的解。

    总结:

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

  • 相关阅读:
    MyBatis之启动分析(一)
    通俗算法教程03
    数学倒底有没有绝对的严格性和形式化?
    通俗算法教程02
    .NET Core 中正确使用 HttpClient 的姿势
    通俗算法教程01
    我要写一篇文章吗?
    [ASP.NET MVC 小牛之路]18
    [ASP.NET MVC 小牛之路]17
    [ASP.NET MVC 小牛之路]16
  • 原文地址:https://www.cnblogs.com/pprp/p/9880042.html
Copyright © 2011-2022 走看看