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

分而治之

实验目的:

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

实验内容:

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

代码:

#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);
        }
    }
}

通过对线段树的概念以及对单点更新实例的介绍,我们能够非常清晰的感受到它将分治思想运用的淋漓尽致,将问题通过二叉树的形式直接分成若干子区间,每一个区间完毕对应的更新,终于汇总成问题的答案

时间: 2024-11-05 18:45:27

南邮算法分析和实验设计1 分而治之的相关文章

南邮算法分析与设计实验2 动态规划法

动态规划法 实验目的: 加深对动态规划法的算法原理及实现过程的理解,学习用动态规划法解决实际应用中的最长公共子序列问题. 实验内容: 用动态规划法实现求两序列的最长公共子序列,其比较结果可用于基因比较.文章比较等多个领域. 实验要求: 掌握动态规划法的思想,及动态规划法在实际中的应用:分析最长公共子序列的问题特征,选择算法策略并设计具体算法,编程实现两输入序列的比较,并输出它们的最长公共子序列. 实验原理及内容(包括操作过程.结果分析等) 1.最长公共子序列(LCS)问题是:给定两个字符序列X=

南邮算法分析与设计实验3 回溯法

回溯法 实验目的: 学习编程实现深度优先搜索状态空间树求解实际问题的方法,着重体会求解第一个可行解和求解所有可行解之间的差别.加深理解回溯法通过搜索状态空间树.同时用约束函数剪去不含答案状态子树的算法思想,会用蒙特卡罗方法估计算法实际生成的状态空间树的结点数. 实验内容: 1.求24点问题 给定四个1-9之间的自然数,其中每个数字只能使用一次,用算术运算符+,-,*,/构造出一个表达式,将这4个正整数连接起来(可以使用括号),使最终的得数为24.要求根据问题的特征设计具体算法并编程实现,输入数据

南邮算法分析与设计实验4 密码算法

实验目的 了解现代密码学的基本原理和数论的基础知识,掌握非对称密码体制的著名代表RSA加密算法的工作原理和流程,并设计实现一个简单的密钥系统. 实验内容 了解加/解密的基本原理和工作过程,用公开密钥对明文进行加密,并用私人密钥对密文进行解密,构造一个简单的 RSA 公开密钥系统. 实验原理 1.RSA算法是由麻省理工学院的 Ron Rivest,Adi Shamir 和Len Adleman 于 1977 年研制并于1978 年首次发表的一种算法,是第一个能同时用于加密和数字签名的算法,且易于理

南邮算法分析与设计实验1 分治策略

分治策略 实验目的: 理解分治法的算法思想,阅读实现书上已有的部分程序代码并完善程序,加深对分治法的算法原理及实现过程的理解. 实验内容: 用分治法实现一组无序序列的两路合并排序和快速排序.要求清楚合并排序及快速排序的基本原理,编程实现分别用这两种方法将输入的一组无序序列排序为有序序列后输出. 代码: #include <iostream> #include <cstdlib> #include <ctime> using namespace std; void Swa

南邮JAVA程序设计实验4 线程程序设计(指针式时钟)

南邮JAVA程序设计实验4  线程程序设计(指针式时钟) 实验目的: 本实验旨在通过实验,培养学生将JAVA 线程的相关知识点(包括线程调度,线程同步等)有机结合并加以综合应用,在实验中设计多线程程序的能力. 实验内容: 设计和编写一个编写一个指针式时钟程序,应用线程实现时钟的走动. 实验设计: 主要是控制时针分针秒针的转动度数,这个直接通过坐标的三角函数值求得,线程方面,隔一秒休眠一下,然后通过时分秒的换算关系来改变三个对应指示针在时钟上的位置 实验代码: import java.awt.*;

“亚信科技杯”南邮第七届大学生程序设计竞赛之网络预赛 (部分题解)

“亚信科技杯”南邮第七届大学生程序设计竞赛之网络预赛 F 自动售货机 时间限制(普通/Java) : 1000 MS/ 3000 MS          运行内存限制 : 65536 KByte总提交 : 60            测试通过 : 13  题目描述 教学楼有一台奇怪的自动售货机,它只售卖一种饮料,单价5元,并且只收5元.10元面值的货币,但是同学们都很喜欢喝.这个售货机里没有多余的找零,也就是说如果一个持有10元的同学第一个购买,则他不能获得5元找零,但是如果在他之前有一个持有5

“Wishare杯”南邮第八届大学生程序设计竞赛之现场决赛 题解报告

A.爆炸吧,现充 (红) 时间限制:1000ms           内存限制:65536K 题目描述: a协有部分脱团分子,日复一日,年复一年地进行秀恩爱虐狗行为,对其他成员持续造成着精神伤害.Kojimai君表示在异端分子长期惨无人道的精神攻击下,早早的患上了少年痴呆症.为了应对这一症状,不得不经常把日常琐事记录下来,时间一长整本笔记本都记完了,他现在好奇自己一共记下了多少字,已知笔记本共n页,每页m行,因为心理因素,他排斥偶数页号,所以只在奇数页号的页面写字,又因为痴呆他在奇数页的第i行都

[python]南邮OJ代码备份爬虫

之前看过Python学习的经验,说以工程为导向学习. 自己分析了一下,一般接触Python的都有一定的其他语言基础,对于程序设计的基本逻辑,语法都有一个大概的了解.而Python这种脚本语言,没有过于独特的语法,在一定的其他语言的基础上,更是可以直接上手的. 之前看Python简明教程,半天没有进度.正好遇上Python爬虫项目,直接上手,方便快捷. 网站:http://acm.njupt.edu.cn/welcome.do?method=index,正值系统更新,于是写一个备份代码的爬虫. 使

南邮JAVA程序设计实验1 综合图形界面程序设计

南邮JAVA程序设计实验1  综合图形界面程序设计 实验目的: 学习和理解JAVA SWING中的容器,部件,布局管理器和部件事件处理方法.通过编写和调试程序,掌握JAVA图形界面程序设计的基本方法. 实验内容: 设计和编写一个用于将人民币转换为等值的美元的程序,界面要求可以输入人民币的金额并可以得到转换后的结果.(每100美元等值买入人民币数:619.72) 实验代码: import java.awt.*; import java.awt.event.*; import java.math.*