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

    总结:

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

  • 相关阅读:
    CSS盒子模型
    getContextPath、getServletPath、getRequestURI、request.getRealPath的区别
    MYSQL中的CASE WHEN END AS
    单点登录的精华总结
    git&github
    June 21st 2017 Week 25th Wednesday
    June 20th 2017 Week 25th Tuesday
    June 19th 2017 Week 25th Monday
    June 18th 2017 Week 25th Sunday
    June 17th 2017 Week 24th Saturday
  • 原文地址:https://www.cnblogs.com/pprp/p/9880042.html
Copyright © 2011-2022 走看看