一、实验目的
1.加深学生对分治法算法设计方法的基本思想、基本步骤、基本方法的理解与掌握;
2.提高学生利用课堂所学知识解决实际问题的能力;
3.提高学生综合应用所学知识解决实际问题的能力。
二、实验任务
1、循环赛日程安排问题(分治算法)
设有n=2k个选手要进行网球循环赛,要求设计一个满足以下要求的比赛日程表:
(1)每个选手必须与其他n-1个选手各赛一次;
(2)每个选手一天只能赛一次。
2、采用分治法求解最大连续子序列和问题
给定一个有n(n≥1)个整数的序列,要求求出其中最大连续子序列的和。
例如:
序列(-2,11,-4,13,-5,-2)的最大子序列和为20
序列(-6,2,4,-7,5,3,2,-1,6,-9,10,-2)的最大子序列和为16。
规定一个序列最大连续子序列和至少是0(长度为0的子序列),如果小于0,其结果为0。
3、采用动态规划算法求解最大连续子序列和问题。
4、采用动态规划算法实现0-1背包问题。
三、实验设备及编程开发工具
实验设备:惠普Win10电脑
开发工具:Java和python环境下,eclipse和pycharm编程工具
四、实验过程设计(算法思路及描述,代码设计)
一.循环赛日程安排问题
基本原理和思路:我们先安排奇数下标位置与偶数下标位置之间的比赛,就有n/2场,方法非常easy,team[2k]=2k,全部奇数号组成一个序列[1,3…n-1],然后循环移动n/2-1次(比方第2个序列就是[3,5…n-1,1]),然后将该序列填充在team的奇数位置上。
接下来将队伍一分为二,奇数为一组,偶数为一组,分配安排其内部比赛(由于奇偶数之间前面已经安排过了啦)。以奇数组[1,3,5,7]为例(以n=8为例说明),我们仍然先安排奇数下标位置与偶数下标位置之间的比赛,也就是[15]与[37]之间的比赛,共同拥有2场(n/4)。
接下来,再将队伍一分为二,得到[15],[37],[04],[26],对每一部分,仍然是先安排奇数下标位置与偶数下标位置之间的比赛,共1场(n/8)。此时已不可再分出子队伍,计算结束。
代码实现如下:
#include<iostream>
#include<vector>
using namespace std;
void GameTable(vector<vector<int> > &vec){
if(vec.size() == 0){
return;
}
size_t s = vec.size();
int k = 0;
while(s = s >> 1){
//s = s >> 1;
k++;
}
//初始化
vec[0][0] = 1;
vec[0][1] = 2;
vec[1][0] = 2;
vec[1][1] = 1;
for(int i = 2; i <= k; i++){
int length = 0x1 << i;
int half = length >> 1;
//左下角的子表中项为左上角子表对应项加half=2^(i-1)
for(int row = 0; row < half; row++){
for(int col = 0; col < half; col++){
vec[row + half][col] = vec[row][col] + half;
}
}
//右上角的子表等于左下角子表
for(int row = 0; row < half; row++){
for(int col = 0; col < half; col++){
vec[row][col + half] = vec[row + half][col];
}
}
//右下角的子表等于左上角子表
for(int row = 0; row < half; row++){
for(int col = 0; col < half; col++){
vec[row + half][col + half] = vec[row][col];
}
}
}
}
int main(void){
cout << "共有2^k个选手参加比赛,输入k(k>0):" << endl;
int k;
do{
cin >> k;
}while(k < 0 || k > 31);
int s = 0x1 << k;
vector<vector<int> > vec(s, vector<int>(s, 0));
GameTable(vec);
for(size_t i = 0; i < vec.size(); i++){
for(size_t j = 0; j < vec[i].size(); j++){
cout << vec[i][j] << " ";
}
cout << endl;
}
return 0;
}
分析:这个过程就是一个填表的过程,因此其时间、空间复杂度为$O(2^k * 2^k) $
二.采用分治法求解最大连续子序列和问题
基本原理和思路:首先,我们可以把整个序列平均分成左右两部分,答案则会在以下三种情况中:
1、所求序列完全包含在左半部分的序列中。
2、所求序列完全包含在右半部分的序列中。
3、所求序列刚好横跨分割点,即左右序列各占一部分。
我们只要计算出:以分割点为起点向左的最大连续序列和、以分割点为起点向右的最大连续序列和,这两个结果的和就是第三种情况的答案。因为已知起点,所以这两个结果都能在O(N)的时间复杂度能算出来。递归不断减小问题的规模,直到序列长度为1的时候,那答案就是序列中那个数字。
代码实现如下:
int fenzhi(int L, int R)
{
if(L==R)
return a[L];
int mid = (L+R)>>1;
int LSum = fenzhi(L,mid);
int RSum = fenzhi(mid+1,R);
int MidSum1 = 0 , MidSum2 = 0,tmp = 0;
for(int i=mid;i>=L; --i)
{
tmp += a[i];
if(tmp>MidSum1)
{
MidSum1 = tmp;
}
}
tmp = 0;
for(int i=mid+1;i<+R;++i)
{
tmp += a[i];
if(tmp>MidSum2)
MidSum2 = tmp;
}
return max(LSum,MidSum1+MidSum2,RSum);
}
分析:分析后得知该程序时间复杂度是O(n),递归的深度是logn, 所以复杂度是O(nlogn)。
三.采用动态规划算法求解最大连续子序列和问题
基本原理和思路:开辟一个二维数组 dp[k][2],其中第一维记录到当前元素为止的最大子序列和,第二维记录该子序列的首元素下标。对于 dp[i][0],只需要考虑两种情况:
1、不取 dp[i-1][0]。也就是说之前的子序列和已经不大于 0 了,那么到当前元素为止的最大子序列和就是当前元素,即 dp[i][0] = arr[i],dp[i][1] = i
2、取 dp[i-1][0]。也就是说之前的子序列和还大于 0,那么到当前元素为止的最大子序列和就是 dp[i][0] = dp[i-1][0] + arr[i],dp[i][1] = dp[i-1][1]
代码实现如下:
#include <cstdio>
#define MAX 10010
int main(){
int n, arr[MAX] = {0}, dp[MAX][2] = {0}; // dp 数组的第二维记录当前子序列的首元素下标
int key = 0; // key 记录 dp 中最大子序列和的存储下标,同时也是最大子序列尾元素的下标
scanf("%d", &n);
for (int i=0; i<n; ++i)
scanf("%d", &arr[i]);
dp[0][0] = arr[0];
for (int i=1; i<n; ++i){
if (dp[i-1][0] <= 0){
dp[i][0] = arr[i];
// 如果不取 dp[i-1][0],那么该子序列首元素为当前元素
dp[i][1] = i;
}
else {
dp[i][0] = dp[i-1][0] + arr[i];
// 如果取 dp[i-1][0],那么该子序列首元素不变
dp[i][1] = dp[i-1][1];
}
// 更新最大子序列首尾元素下标
(dp[i][0] > dp[key][0]) ? (key = i, dp[key][1] = dp[i][1]) : 0;
}
if (dp[key][0] < 0)
printf("%d %d %d\n", 0, arr[0], arr[n-1]);
else
printf("%d %d %d\n", dp[key][0], arr[dp[key][1]], arr[key]);
return 0;
}
分析:间复杂度为 O ( n 2 ) O(n^2) O(n2)
四.采用动态规划算法实现0-1背包问题
基本原理和思路:0-1背包问题的关键在于递归过程之中是进行物品i是否能放入背包之中,若物品i不能放入背包中时,子问题a(i-1,j)一定是子问题a(i,j)的最优解,如果i可以放入背包之中,那么要判断是否再放入物品之后的价值与放入之前相比是否有增加,若有增加则选择更多质量的那种放置方式,如果没有增加,则与之前的价值保持一致。
代码实现如下:
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.Scanner;
public class knapsack {
int weight;//背包所能承受的重量
int number;//物品的个数值
String[] scale=new String[500];//每个物品的质量大小
String[] value=new String[500];//每个物品的价值大小
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
knapsack Knaspack=new knapsack();
Knaspack.input();//对数据进行读取,写入信息
int[][] p=Knaspack.init();//初始化背包
int[][] r=Knaspack.planning(p);//采用动态规划的思想获取最大价值的物品
System.out.println();
System.out.println("求解过程");
for(int i=0;i<r.length;i++) {
for(int j=0;j<r[i].length;j++) {
System.out.print(r[i][j]+" ");
}
System.out.println();
}
System.out.println();
System.out.println("此时背包中的最大价值为:"+r[Knaspack.number][Knaspack.weight]);
}
/*1.对背包的初始化,当背包所存放的物品数量为0时,价值默认设置为0
* 当背包所能储存的重量为0时,对于价值全设置为0
* 返回初始化成功的背包
*/
public int[][] init() {//初始化背包值
int a[][]=new int[this.number+1][this.weight+1];
for(int i=0;i<=this.number;i++)//此时背包的重量为0,价值也为0
a[i][0]=0;
for(int j=0;j<=this.weight;j++)//此时所装数量为0,价值也为0
a[0][j]=0;
return a;
}
/*2.0-1背包核心算法内容
*首先双循环,先通过数量的变化,然后重量的变化对于现有的物品质量进行判断是否存在有能够放得下且价值提升的物品
*如果没有物品可以放入直接等于数量-1,重量不变的价值
*可以放的下的情况分为两种可能性,与放入之前的价值进行判断,如果大于放入之前的值,则选择新放入方法,提升价值
*如果不变或者小于,价值仍与数量-1的那个价值一致,不发生变化
*
*/
public int[][] planning(int[][] a){
for(int i=1;i<=this.number;i++) {
for(int j=1;j<=this.weight;j++) {
if(Integer.parseInt(this.scale[i-1])>j) {
a[i][j]=a[i-1][j];
}
else {
if(a[i-1][j]<a[i-1][j-Integer.parseInt(this.scale[i-1])]+Integer.parseInt(this.value[i-1])) {
a[i][j]=a[i-1][j-Integer.parseInt(this.scale[i-1])]+Integer.parseInt(this.value[i-1]);
}
else {
a[i][j]=a[i-1][j];
}
}
}
}
return a;
}
/*3.读入.txt文件
* 首先输入想要测试的那个样本数字
* 拼接字符串之后读取文件内容
* 第一行读入背包的重量,第二行读入物品的数量
* 第三行第四行是物品重量及其价值的对应关系
* 通过,分隔字符串的方法来放入数组之中等待操作
*
*/
public void input() throws Exception {
System.out.println("请输入1-7之间的一个数");
Scanner sc=new Scanner(System.in);
String num=sc.nextLine();
FileReader filereader=new FileReader("input0"+num+".txt");
BufferedReader buf=new BufferedReader(filereader);
String readline="";
readline=buf.readLine();
this.weight=Integer.parseInt(readline);
readline=buf.readLine();
this.number=Integer.parseInt(readline);
System.out.println("背包所能承受的重量为:"+weight+" 一共有"+number+"个物品可以选择装入背包");
System.out.println("物品重量及其价值对应关系:");
readline=buf.readLine();
scale=readline.split(",");
for(int i=0;i<this.number;i++) {
System.out.print(scale[i]+",");
}
System.out.println();
readline=buf.readLine();
value=readline.split(",");
for(int i=0;i<this.number;i++) {
System.out.print(value[i]+",");
}
}
}
时间复杂度为O(nc),其中n表示物品的个数,c表示背包的容量。空间的效率就是用于存储二维数组的占用空间大小,即为O(nc).
五、实验小结(包括问题和解决方法、心得体会等)
经过这次试验收获颇多,代码实现过程中也遇到一些问题,有的问题确实也有一定的难度,所以也是通过了网络搜索才得出的解决方案,思维上得到了很好的训练,同时也明白了一个道理:纸上得来终觉浅,绝知此事要躬行。尤其是算法和编程这门课程更是要勤于动手方能获得收获。希望下次实验或者之后的编程学习能吸取这些教训。