您的位置:首页 > 其它

南邮算法分析和实验设计1 分而治之

2015-06-11 09:56 405 查看
分而治之

实验目的:

理解分治法的算法思想,阅读实现书上已有的部分程序代码并完好程序,加深对分治法的算法原理及实现过程的理解。

实验内容:

用分治法实现一组无序序列的两路合并排序和高速排序。要求清楚合并排序及高速排序的基本原理,编程实现分别用这两种方法将输入的一组无序序列排序为有序序列后输出。

代码:

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

void Swap(int &a, int &b)
{
int t = a;
a = b;
b = t;
}

template <class T>
class SortableList
{
public:
SortableList(int m)
{
n = m;
}
void MergeSort();
void Merge(int left, int mid, int right);
void QuickSort();
void Input();
void Init();
void Output();
private:
int RPartition(int left, int right);
int Partition(int left, int right);
void MergeSort(int left, int right);
void QuickSort(int left, int right);
T l[1000];//输入的数组值
T a[1000];//实际排序对象
int n;
};

template<class T>
void SortableList<T>::Input()
{
for(int i = 0; i < n; i++)
cin >> l[i];
}
//Init()函数的作用是在两路合并排序结束后将序列恢复到初始序列
//再进行高速排序
template<class T>
void SortableList<T>::Init()
{
for(int i =0; i < n; i++)
a[i] = l[i];
}
template<class T>
void SortableList<T>::Output()
{
for(int i = 0; i < n; i++)
cout << a[i] << " ";
cout << endl << endl;
}

//两路合并排序
template<class T>
void SortableList<T>::MergeSort()
{
MergeSort(0, n - 1);
}
template<class T>
void SortableList<T>::MergeSort(int left, int right)
{
if(left < right)
{
int mid = (left + right) / 2;
MergeSort(left, mid);
MergeSort(mid + 1, right);
Merge(left, mid, right);
}
}
template <class T>
void SortableList<T>::Merge(int left, int mid, int right)
{
T* temp =new T[right - left + 1];
int i = left, j = mid + 1, k = 0;
while((i <= mid)&&(j <= right))
if(a[i] <= a[j])
temp[k ++] = a[i ++];
else
temp[k ++] = a[j ++];
while(i <= mid)
temp[k ++] = a[i ++];
while(j <= right)
temp[k ++] = a[j ++];
for(i = 0, k = left; k <= right;)
a[k ++] = temp[i ++];
}

//高速排序
template <class T>
int SortableList<T>::RPartition(int left, int right)
{
srand((unsigned)time(NULL));
int i = rand() % (right - left) + left;
Swap(a[i], a[left]);
return Partition(left, right);
}
template <class T>
int SortableList<T>::Partition(int left, int right)
{
int i = left, j = right + 1;
do
{
do i ++; while(a[i] < a[left]);
do j --;	while(a[j] > a[left]);
if(i < j)
Swap(a[i], a[j]);
}while(i < j);
Swap(a[left], a[j]);
return j;
}
template <class T>
void SortableList<T>::QuickSort(int left, int right)
{
if(left < right)
{
int j = RPartition(left, right);
QuickSort(left, j - 1);
QuickSort(j + 1, right);
}
}
template<class T>
void SortableList<T>::QuickSort()
{
QuickSort(0, n - 1);
}

int main()
{
int m;
cout << "数组长度n: ";
cin >> m;
SortableList<int> List(m);
cout << "输入" << m << "个数字:" << endl;
List.Input();
List.Init();//得到初始状态
List.MergeSort();
cout << "两路合并排序后:" << endl;
List.Output();
List.Init();//恢复初始状态
cout << "高速排序后:" << endl;
List.QuickSort();
List.Output();
return 0;

}


两者比較:

问题分解过程:

合并排序——将序列一分为二就可以。 (十分简单)

高速排序——需调用 Partition 函数将一个序列划分为子序列。(分解方法相对较困难)

子 问 题 解合并得到原 问题解的过程:

合并排序——须要调用 Merge 函数来实现。(Merge 函数时间复杂度O(n))
高速排序——一旦左、右两个子序列都已分别排序,整个序列便自然成为有序序列。(异 常简单,差点儿无须额外的工作,省去了从子问题解合并得到原问题解的过程)

思考题:

1、在上述高速排序算法的运行过程中,跟踪程序的运行会发现,若初始输入序列递减有序, 则调用 Partition 函数进行分划操作时,下标 i 向右寻找大于等于基准元素的过程中会产生 下标越界,为什么?怎样改动程序,能够避免这样的情况的发生?

答:这是由于原有的程序在序列最右边没有设一个极大值作为哨兵,则下标 i 在向右寻找大 于等于基准元素的过程中,一直没有满足条件的元素值出现,就一直不会停止,直至越界。

所以仅仅要在序列的最后预留一个哨兵元素,将它的值设为一个极大值比方INT_MAX就能够解决

2、分析这两种排序算法在最好、最坏和平均情况下的时间复杂度。

两路合并排序:最好、最坏、平均情况下的时间复杂度均为 O(nlogn)。

高速排序:最好、平均情况下的时间复杂度为 O(nlogn),最坏情况下为 O(n2)。

3、当初始序列是递增或递减次序排列时,通过改进主元(基准元素)的选择方法,能够提 高高速排序算法执行的效率,避免最坏情况的发生。

有三种主元选择方式。 一是取 K(left+right)/2 为主元;

二是取 left~right 之间的随机整数 j,以 Kj 作为主元;

三是取 Kleft、K(left+right)/2 和 Kright 三者的中间值为主元。

实验程序採取的是另外一种方法

分治法实例 线段树

谈到分治法首先想到的肯定是二分搜索。以及高速排序,两路合并排序,这些都是经典的採用分治策略解决这个问题的算法。我要介绍的是一种很方便的採用了分治法思想的数据结构——线段树

线段树实质上是一棵二叉搜索树,它非常方便的攻克了非常多区间操作的问题。线段树既是线段也是树。每一个结点是一条线段,每条线段的左右儿子线段各自是该线段的左半和右半区间,递归定义之后就是一棵线段树,首先我们先要知道这种数据结构用什么来存储,通常使用一个结构体数组:

struct Tree            //树结构体
{
    int left, right;    //树的左右子树
}tree[MAX];
//MAX通常设为节点数的4倍


接下来看一下基础线段树中的几个函数:

1.Build函数,顾名思义就是建树函数,通过这个函数我们能够建立一棵线段树。

void Build(int l, int r, int step)  //建树,step代表树节点的编号(下面均是)
{
    tree[step].left = l;    //赋值
    tree[step].right = r;
    if(l ==r)    //l == r时说明伸展到叶子节点,返回
        return;
    int mid = (l+ r) >> 1;   //二分建树
    Build(l,mid, step<<1);        //递归左子树
    Build(mid+1,r, (step<<1)+1);  //递归右子树
}


2. Update函数。用来更新线段树

void Update(int l, int r, int value, int step)
{
    if(tree[step].left== tree[step].right)   //一直更新到叶子节点返回
        return;
    int mid = (tree[step].left +tree[step].right) >> 1;
    //假设所要更新的点的右端点小于mid或左端点大于mid,则直接更新l到r的值
    if(r <= mid)  
        Update(l,r, value, step<<1);
    else if(l> mid)
        Update(l,r, value, (step<<1)+1);
    //假设所要更新的点在mid的两边,则两边分别更新
    else
    {   
        Update(l, mid, value, step<<1);
        Update(mid+1, r, value, (step<<1)+1);
    }
}


3. Query函数,用来查询区间值

int Query(int l, int r, int step)
{
    //找到叶子返回叶子值
    if(l == tree[step].left && r ==tree[step].right)
        return tree[step].值;
    int mid = (tree[step].left +tree[step].right) >> 1;
    //查询和更新相似。都是分段操作
    if(r <=mid)
        return Query(l, r, step<<1);
    if(l >mid)
        return Query(l, r, (step<<1)+1);
    else
        return Query(l, mid, step<<1)+Query(mid+1, r, (step<<1)+1);
}


以上三个函数构成了线段树的基本结构。我们能够非常清楚的看出它的分治思想。线段树有非常多种。比方值的单点更新和值的区间更新等,我们这里就通过一个实例简介线段树里最简单的一种应用及线段树单点更新,顾名思义,每次仅仅更新一个节点的值,以下来看这个问题,来自HDUOJ 1166

敌兵布阵

Problem Description

C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又開始忙乎了。A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些 工兵营地的活动情况。因为採取了某种先进的监測手段。所以每一个工兵营地的人数C国都掌握的一清二楚,每一个工兵营地的人数都有可能发生变动,可能添加或降低 若干人手,但这些都逃只是C国的监视。

中央情报局要研究敌人到底演习什么战术,所以Tidy要随时向Derek汇报某一段连续的工兵营地一共同拥有多少人,比如Derek问:“Tidy,立即汇报 第3个营地到第10个营地共同拥有多少人!”Tidy就要立即開始计算这一段的总人数并汇报。但敌兵营地的人数常常变动,而Derek每次询问的段都不一样。 所以Tidy不得不每次都一个一个营地的去数,非常快就精疲力尽了。Derek对Tidy的计算速度越来越不满:"你个死肥仔,算得这么慢,我炒你鱿 鱼!”Tidy想:“你自己来算算看,这可真是一项累人的工作!我恨不得你炒我鱿鱼呢!”无奈之下,Tidy仅仅好打电话向计算机专家
Windbreaker求救,Windbreaker说:“死肥仔,叫你平时做多点acm题和看多点算法书。如今尝到苦果了吧!”Tidy说:"我知错 了。。。"但Windbreaker已经挂掉电话了。Tidy非常苦恼,这么算他真的会崩溃的,聪明的读者。你能写个程序帮他完毕这项工作吗?只是假设你的 程序效率不够高的话,Tidy还是会受到Derek的责骂的.

Input

第一行一个整数T,表示有T组数据。

每组数据第一行一个正整数N(N<=50000),表示敌人有N个工兵营地,接下来有N个正整数,第i个正整数ai代表第i个工兵营地里開始时有ai个人(1<=ai<=50)。

接下来每行有一条命令,命令有4种形式:

(1) Add i j,i和j为正整数,表示第i个营地添加j个人(j不超过30)

(2)Sub i j ,i和j为正整数,表示第i个营地降低j个人(j不超过30);

(3)Query i j ,i和j为正整数,i<=j,表示询问第i到第j个营地的总人数;

(4)End 表示结束,这条命令在每组数据最后出现;

每组数据最多有40000条命令

Output

对第i组数据,首先输出“Case i:”和回车,

对于每一个Query询问,输出一个整数并回车,表示询问的段中的总人数,这个数保持在int以内。

Sample Input

1

10

1 2 3 4 5 6 7 8 9 10

Query 1 3

Add 3 6

Query 2 7

Sub 10 2

Add 6 3

Query 3 10

End

Sample Output

Case 1:

6

33

59

题目分析:题意非常easy,就是给一个区间有三个操作。Add表示加人,Sub表示减人,Query表示查询区间人数和。对于这样的区间的更新查询类问题。一下就能想到上面介绍的线段树,基本操作部分就不写凝视了。上面已经介绍过,以下看AC代码

#include <cstdio>
#include <cstring>
int const MAX = 400000 + 10;
struct Tree            //树结构体
{
    int left, right;     //树的左右子树
    int sum;            //树的值
}tree[MAX];
void Build(int l, int r, int step)  //建树,step代表树节点的编号(下面均是)
{
    tree[step].left = l;    
    tree[step].right = r;
    tree[step].sum = 0;
    if(l == r)    //l == r时说明伸展到叶子节点,返回
        return;
    int mid = (l + r) >> 1;   //二分建树
    Build(l, mid, step<<1);
    Build(mid+1, r, (step<<1)+1);
}

void Update(int l, int r, int value, int step)  //更新函数
{
    tree[step].sum += value;   //由于这里是求和,所以直接将遍历到的点数值加上所传值
    if(tree[step].left == tree[step].right)   //一直更新到叶子节点返回
        return;
    int mid = (tree[step].left + tree[step].right) >> 1;
    if(r <= mid)   //假设所要更新的点的右端点小于mid,则直接更新l到r的值
        Update(l, r, value, step<<1);
    else if(l > mid)  //假设所要更新的点的左端点大于mid,则直接更新l到r的值
    //注意上面不能写成 r<mid 和 l>=mid 由树的性质决定。读者能够绘图看出
        Update(l, r, value, (step<<1)+1);
    else  //假设所要更新的点在mid的两边,则两边分别更新
    {
        Update(l, mid, value, step<<1);
        Update(mid+1, r, value, (step<<1)+1);
    }
}

int Query(int l, int r, int step)  //查询函数
{
    if(l == tree[step].left && r == tree[step].right) //找到叶子返回叶子值
        return tree[step].sum;
    int mid = (tree[step].left + tree[step].right) >> 1;//下面相似更新步骤,不再阐述
    if(r <= mid)
        return Query(l, r, step<<1);
    if(l > mid)
        return Query(l, r, (step<<1)+1);
    else
        return Query(l, mid, step<<1) + Query(mid+1, r, (step<<1)+1);
}

int main()
{
    int T;
    int a, b, n;
    char cmd[6];
    scanf("%d",&T);
    for(int i = 1; i <= T; i++)
    {
        scanf("%d",&n);
        Build(1,n,1);  //1-n建树
        for(int j = 1; j <= n; j++)
        {
            int temp;
            scanf("%d",&temp);
            Update(j,j,temp,1);   //从根一直更新到叶子
        }
        printf("Case %d:\n",i);
        while(scanf("%s", cmd) != EOF && strcmp(cmd,"End") != 0)
        {
            scanf("%d %d",&a, &b);
            if(strcmp(cmd,"Query") == 0)
                printf("%d\n", Query(a,b,1));
            else if(strcmp(cmd,"Add") == 0)  //加的时候b的值为正
                Update(a,a,b,1);
            else if(strcmp(cmd,"Sub") == 0)  //减的时候b的值为负
                Update(a,a,-b,1);
        }
    }
}


通过对线段树的概念以及对单点更新实例的介绍,我们能够非常清晰的感受到它将分治思想运用的淋漓尽致,将问题通过二叉树的形式直接分成若干子区间,每一个区间完毕对应的更新,终于汇总成问题的答案
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: