实验一 分治策略——归并排序
- 实验要求
(1)编写一个模板函数:
template <typename T>
MergeSort(T *a, int n);
以及相应的一系列函数,采用分治策略,对任意具有:
bool operator<(const T& x,const T& y) 比较运算符的数据类型,均进行排序。
(2)与STL库中的函数std::sort(..)进行运行时间上的比较,给出比较结果,
如:动态生成100万个随机生成的附点数序列的排序列问题, 给出所用的时间比
较。
二、实验正文
1.算法思想
(1)分治策略的思想是将一个大问题不断划分成小问题,分别解决小问题,并把小问题的解组合起来就能获得原问题的解。
(2)归并排序的基本思想是当n=1时终止划分,否则持续将数据分成大小相同的两个子集。分别对两个子集排序,并用排序算法将两个子集一起排序,从而得到完全排序的数据。
2.重要代码
template<typename T>
void MergeSortArr(T* a, int left, int mid, int right)
{
int* temp = new int[right - left + 1]();
int i = left;
int j = mid + 1;
int k = 0;
while (i <= mid && j <= right)
{
if (a[i] <= a[j])
temp[k++] = a[i++];
else if (a[i] > a[j])
temp[k++] = a[j++];
}
while (i <= mid)
temp[k++] = a[i++];
while (j <= right)
temp[k++] = a[j++];
k = 0;
for (int i = left; i <= right; i++)
{
a[i] = temp[k++];
}
}
3.运行结果
Sort排序:
归并算法排序:
Sort排序所用时间更长。
三、综合实验总结
- 实验难点
- 理解分治策略思想
- 理解归并排序思想
- 心得体会
通过本次实验,我对分治策略的理解更加深刻,对归并排序的使用也更加熟练。
实验二 贪心算法—Huffman树及Huffman编码
- 实验要求
1. 要求能够对任意指定的文件(不能仅文本文件,是任意!),读出并统计文件中出现的字符及个数;
2. 对此文件中出现的字符进行Huffman编码,并输出。
即:编写一个读取此种格式文件类CHuffman, 内部机制采用优先队列,用于建立
Huffman 树及进行Huffman编码输出,对于输出树的形式可自行决定(如图形界面或字符界面)。
二、实验正文
1.算法思想
(1)贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
(2)使用优先队列解决哈夫曼编码问题,从文件中获取字符后,将字符存入数组中,并放入优先队列。每次取出最小的两个节点合成一个父节点,进入队列和数组,共进行n-1次。再通过各节点的父子关系,找到每个字符回应的哈夫曼编码。
2.重要代码
void haff(node* a, int n) //贪心法求哈夫曼编码
{
priority_queue <node, vector<node>, greater<node> > PQ;
for (int i = 0; i < n; i++)
{
a[i].idx = i;
PQ.push(a[i]);
}
int k = n;
for (int i = 1; i <= (n - 1); i++)
{
node L = PQ.top();
PQ.pop();
node R = PQ.top();
PQ.pop();
a[k].idx = k;
a[k].p = -1;
a[k].f = L.f + R.f;
a[k].l = L.idx;
a[k].r = R.idx;
a[L.idx].p = k;
a[R.idx].p = k;
PQ.push(a[k]);
k++;
}
for (int i = 0; i < n; i++)
{
cout << "字符为:"<<a[i].c << " 编码为:";
vector<int> v;
node p = a[i];
while (1)
{
node pp = a[p.p];
if (p.idx == pp.l)
v.push_back(0);
else
v.push_back(1);
if (pp.p == -1)break;
p = pp;
}
int s = v.size();
for (int j = s - 1; j >= 0; j--)
{
cout << v[j];
}
cout << endl;
}
}
3.运行结果
三、综合实验总结
- 实验难点
- 理解贪心策略思想
- 理解哈夫曼编码思想
- 心得体会
通过本次实验,我对分治贪心算法有了更深入的理解,用优先队列帮助构建哈夫曼树为算法提供了便利。
实验三 用动态规划法求:矩阵链乘积问题
- 实验要求
对任意给定的n+1个整数r 0 , r1 ,... r n , 求出矩阵链乘积的最优方案。输出带括号的乘积字符串。
二、实验正文
1.算法思想
(1)动态规划算法是将问题实例分解为更小的,相似的子问题,并存储子问题而避免计算重复,来解决最优化问题的算法策略。
(2)通过分析矩阵链乘积,得到状态转移方程,构建二维数组,不断记录,并通过已经记录有的数据求得更多数据。先将对角线值都置0,再斜向上计算A[i][i+1]···,最后求得从第一个元素到最后一个元素的划分方法。最后通过递归输出划分方式。
A[i][j]= 0 , i=j
P[i-1]*p[i]*p[i+1] ,j=i+1
Min(A[i][k]+A[k+1][j]+p[i-1]*p[k]*p[j])(i<=k<j) ,j>i+1
2.重要代码
template<typename T>
void LC(node** A, T* p, T n)
{
for (int i = 1; i <= n; i++)
A[i][i].data = 0;
for (int i = 1; i < n; i++)
{
A[i][i + 1].data = p[i - 1] * p[i] * p[i + 1];
A[i][i + 1].count = i;
}
for (int i = 2; i <= n; i++)
{
for (int j = 1; j < (n - i + 1); j++)
{
int r = i + j - 1;
A[j][r].data = A[j][j].data + A[j + 1][r].data + p[j - 1] * p[j] * p[r];
A[j][r].count = j;
for (int k = (j + 1); k < r; k++)
{
int temp = A[j][k].data + A[k + 1][r].data + p[j - 1] * p[k] * p[r];
if (temp < A[j][r].data)
{
A[j][r].data = temp;
A[j][r].count = k;
}
}
}
}
}
}
3.运行结果
三、综合实验总结
- 实验难点
- 理解动态规划策略思想
- 理解矩阵连乘积问题思想
- 心得体会
动态规划算法大部分需要靠二维数组记录数据来实现,找到转移方程后,与数组结合即可求出最后解。
实验四 用回溯(递归回溯)方法求解n后问题
- 实验要求
问题:对任意给定的n(<=14),采用递归回溯方法求解n后问题。
具体要求:
1.封装n后问题为类:
2.对任意给定的n,要求输出其解向量(所有解),并输出其解数;
3.构造n后问题的解数表格(由程序自动生成)
二、实验正文
1.算法思想
(1)回溯法采用试错的思想,尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步或上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现。
(2)题目要求皇后不能在同行,同列,同对角线上,这是约束条件。当k>n时,说明问题已解决,输出解。当k<=n时,如果满足约束条件,则递归求k+1时情况。
2.重要代码
void nqueen(int* x, int n, int k,int &count) //回溯法解n后问题
{
if (k > n)
{
count++;
cout << "第" << count << "个解:";
for (int i = 1; i <= n; i++)
cout << x[i] << " ";
cout << endl;
}
else
{
for (int i = 1; i <= n; i++)
{
x[k] = i;
if (Canplace(x, k))
nqueen(x, n, (k + 1), count);
}
}
}}
3.运行结果
三、综合实验总结
- 实验难点
- 理解回溯法思想,理解回溯法框架
- 理解n后问题思想
- 心得体会
回溯算法基本遵循回溯框架,理解回溯算法框架后,对回溯问题的解决思路大致相同。
实验五 背包(普通或非0-1背包)问题的贪心算法
- 实验要求
给定如下n种物品的编号,及其价值;背包重量为c, 求最佳装包方案,才
能使其装入背包的价值最大。
具体要求:
(1)将背包问题进行类的封装;
(2)能对任意给定的n种物品的重量、价值及背包限重,输出以上表格( 或
纵向输出);
(3)输出背包问题的解;
(4)本题要求采用STL库中的排序算法数据进行排序。
二、实验正文
1.算法思想
贪心算法只考虑眼前利益,算出每个物品的单位价值进行排序,从而得到近似最优解。因为是普通背包问题,物品可部分装入。排序后,从最大单位价值依次装入,即得最优解。
2.重要代码
void bag(node* a, int c, int n)
{
for (int i = 0; i < n; i++)
{
a[i].p = a[i].v / a[i].w * 1.0;
}
sort(a, a + n, compare);
for (int i = 0; i < n; i++)
{
if (c > a[i].w)
{
c -= a[i].w;
a[i].n = 1;
}
else if (c > 0)
{
a[i].n = c / a[i].w * 1.0;
c = 0;
}
}
}
3.运行结果
三、综合实验总结
- 实验难点
- 普通背包可取部分商品
- 心得体会
贪心法解背包问题,就是将物品按单位价值排序,从大到小依次加。
实验六 0-1背包问题的求解
- 实验要求
0-1 背包问题有多种解法,如动态规划方法,回溯方法,分枝限界方法等。
对于同一种问题,请参照教材中的算法,给出相应的程序实现。
要求:(1)回溯方法;(2)分枝限界方法。
二、实验正文
1.算法思想
(1)回溯方法求01背包,就是从第一个开始,如果能加,就考虑加和不加两种情况。每次加第k个物品后,要恢复原状态,来进行没有加第k个物品的另一种情况。
(2)分支限界法求01背包借助优先队列实现,首先,要对输入数据进行预处理,将各物品依其单位重量价值从大到小进行排列。在实现时,由Bound计算当前结点处的上界。在解空间树的当前扩展结点处,仅当要进入右子树时才计算右子树的上界Bound,以判断是否将右子树剪。进入左子树时不需要计算上界,因为其上界与其父节点上界相同。在优先队列分支限界法中,结点的优先级定义为:以结点的价值上界作为优先级
2.重要代码
void backtrack(int* w, int* v, int* x, int* y, int n, int cw, int cv, int k, int c, int& max) //回溯法解01背包问题
{
if (k > n)
if (cv > max)
{
max = cv;
for (int i = 0; i < n; i++)
y[i] = x[i];
}
else
{
for (int i = 0; i <= 1; i++)
{
x[k] = i;
if ((cw + x[k] * w[k]) <= c)
{
cv += x[k] * v[k];
cw += x[k] * w[k];
backtrack(w, v, x, y, n, cw, cv, (k + 1), c, max);
cw -= x[k] * w[k];
cv -= x[k] * v[k];
}
}
}
}
void MaxKnapsack()
{
priority_queue<MaxHeapQNode*, vector<MaxHeapQNode*>, cmp > q; // 大顶堆
MaxHeapQNode* E = NULL;
cw = cp = bestp = 0;
int i = 1;
int up = Bound(1); //Bound(i)函数计算的是i还未处理时候的上限值
while (i != n + 1)
{
int wt = cw + obj[i].weight;
if (wt <= c)
{
if (bestp < cp + obj[i].price)
bestp = cp + obj[i].price;
AddAliveNode(q, E, up, cw + obj[i].weight, cp + obj[i].price, i, 1);
}
up = Bound(i + 1); //注意这里 up != up - obj[i].price而且 up >= up - obj[i].price
if (up >= bestp) //注意这里必须是大于等于
{
AddAliveNode(q, E, up, cw, cp, i, 0);
}
E = q.top();
q.pop();
cw = E->weight;
cp = E->profit;
up = E->upprofit;
i = E->lev;
}
for (int j = n; j > 0; --j)
{
bestx[obj[E->lev - 1].id] = E->lchild;
E = E->parent;
}
}
3.运行结果
三、综合实验总结
- 实验难点
- 理解分支限界法思想
- 心得体会
解分支限界问题的步骤为:
(1)算法首先根据基于可行结点相应的子树最大价值上界优先级,从堆中选择一个节点(根节点)作为当前可扩展结点。
(2)检查当前扩展结点的左儿子结点的可行性。
(3)如果左儿子结点是可行结点,则将它加入到子集树和活结点优先队列中。
(4)当前扩展结点的右儿子结点一定是可行结点,仅当右儿子结点满足上界函数约束时,才将它加入子集树和活结点优先队列。
(5)当扩展到叶节点时,算法结束,叶子节点对应的解即为问题的最优值。
实验七 第k小问题
实验要求
采用类快排的方法,编写分区函数:intpartition(T*a,intl,intr)
并求第k小的问题。这是一种典型的分治策略
二、实验正文
1.算法思想
(1)使用position分区算法不断分区,分成左小右大两部分。
(2)再通过分治算法,判断k在左部分还是右部分,然后继续在k所在的那部分分区,直到找到看。
2.重要代码
template<typename T>
void mink(T* a, T k, T l, T r) //分治法求第k小问题
{
int i = l, j = r;
int temp = a[l];
while (i != j)
{
while (i < j && a[j] >= temp)
j--;
a[i] = a[j];
while (i < j && a[i] <= temp)
i++;
a[j] = a[i];
}
a[i] = temp;
if (k == (i + 1))
cout << a[i];
else if (k < (i + 1))
mink(a, k, l, i);
else if (k > (i + 1))
mink(a, k, (i + 1), r);
}
3.运行结果
三、综合实验总结
- 实验难点
- 掌握position分区算法
- 心得体会
通过查找第k小元素实验,复习了快速排序算法思想,对排序的理解更加透彻。
实验八 最大子段和问题的求解
实验要求
对任意动态生成的n 个整数(可含负数),求最大子段及其和。
1.采用至少三种方法进行求解:
(1) 蛮力方法(枚举方法);
(2) 分治策略;
(3)动态规划方法。
2.对算法和数据进行类的封装,编写好构造函数和析构函数;
3.对任意给定的n个整数,要求对以上的三种算法,都能够输出最大子段及其和。
二、实验正文
1.算法思想
(1)枚举法,通过将所有可能情况全部列举出来,来找到最大子段和。
(2)分治法,通过递归分别求左边最大,右边最大,中间最大,比较得出最大子段和。
(3)动态规划法,通过一个一维数组,存储以a[i]为末尾元素的最大子段和,全部记录完后,通过比较得到最大子段和。
2.重要代码
int maxSum(int n, int a[], int& besti, int& bestj) { //枚举方法
int sum = 0;
for (int i = 0; i < n; i++) {
int thisSum = 0;
for (int j = i; j < n; j++) {
thisSum += a[j];
if (thisSum > sum) {
sum = thisSum;
besti = i;
bestj = j;
}
}
}
return sum;
}
template<typename T>
int maxlen(T* a, T l, T r) //分治法求最大子段和
{
int maxl, maxr, sum = 0, lsum = 0, rsum = 0, mlsum = 0, mrsum = 0, msum;
if (l == r)
return a[l];
else
{
int mid = (l + r) / 2;
int maxl = maxlen(a, l, mid);
int maxr = maxlen(a, (mid + 1), r);
for (int i = mid; i >= l; i--)
{
lsum += a[i];
if (lsum > mlsum)
mlsum = lsum;
}
for (int i = (mid + 1); i <= r; i++)
{
rsum += a[i];
if (rsum > mrsum)
mrsum = rsum;
}
if (maxl >= maxr)
sum = maxl;
msum = mlsum + mrsum;
if (msum > sum)
sum = msum;
return sum;
}
}
template<typename T>
void bigsum(T* dp, T* a, T n) //动态规划求最大子段和
{
dp[0] = a[0];
for (int i = 1; i < n; i++)
{
int temp = dp[i - 1] + a[i];
if (temp > a[i])
{
dp[i] = temp;
}
else
{
dp[i] = a[i];
}
}
3.运行结果
三、综合实验总结
- 实验难点
- 理解三种方法思想
- 心得体会
通过三种求最大子段和方法的比较,我对求最大子段和这个问题有了更清楚的认知。