文章目录
- 第1关:贪心法
- 代码
- 第2关:最小生成树
- 代码
- 第3关:Huffman 编码
- 代码
- 第4关:单源点最短路径
- 代码
第1关:贪心法
相关知识
为了完成本关任务,你需要掌握:贪心法 ;。
贪心法,又称贪婪算法是一种解决问题的策略。是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。如果策略正确,那么贪心法往往是易于描述、易于实现的。
贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
贪心法的优缺点
贪心法可以解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码……对于其他问题,贪心法一般不能得到我们所要求的答案。一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。
下面通过一个简单的实例题目对贪心进行详解:
例题:
乘船问题: 给出 n 个人,第 i 个人重量为 w
i
。每艘船的最大载重量均为 C,且最多只能乘两个人。用最少的船装载所有人。有解输出船的个数,无解输出 “no”。
解题分析
考虑最轻的人i,如果每个人都无法和他一起坐船,则唯一的方法就是每个人一艘船。否则,他应该选择能和他一起坐船的人中最重的一个j。这样的方法是贪心的,因此它只是让眼前的浪费最少。
程序实现
将人的重量按照从小到大排序。比j更重的人只能每人坐一艘船。这样,只需要两个下表i和j分别表示当前考虑的最轻的人和最重的人,每次先将j往左移动,直到i和j可以共坐一艘船,然后将i+1,j-1,并重复上述操作。
关键代码
int p=0,q=n-1,s=0; // p代表左索引,q代表右索引
if(a[q]>c) cout<<“no”<<endl; // 如果最大的体重(最右边索引的人)大于船的最大载重,那么直接输出 no
else{
for(int i=0;i<n;i++){ // 遍历所有人
if(p>q) break;
if(a[p]+a[q]>c) q–; // 如果最小体重的人和当前最大的人不能同船,那么当前体重最大的人就要一个人一个船。
else p++,q–; // 当前体重最小的人和当前体重最大的人同船
s++;
}
cout<<s<<endl;
}
时间复杂度:不难看出,程序的时间复杂度仅为O(n)。是最优算法。
代码
#include<iostream>
using namespace std;
const int maxn = 10000;
void fast_sort(int * a, int begin, int end) {
int l, r, m;
l = begin, r = end, m = (begin + end) >> 1;
while (l <= r) {
while (a[l] < a[m]) l++;
while (a[r] > a[m]) r--;
if (l <= r) {
int t = a[l];
a[l] = a[r];
a[r] = t;
l++;
r--;
}
}
if (l < end) fast_sort(a, l, end);
if (begin < r) fast_sort(a, begin, r);
}
int main() {
int n, c;
int a[maxn];
/********** Begin **********/
cin >> n >> c;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
fast_sort(a, 0, n - 1);
int p = 0, q = n - 1, s = 0;
if (a[q] > c) cout << "no" << endl;
else {
for (int i = 0; i < n; i++) { // 遍历所有人
if (p > q) break;
if (a[p] + a[q] > c) q--;
else p++, q--;
s++;
}
cout << s << endl;
}
/********** End **********/
return 0;
}
第2关:最小生成树
一个连通图可能有多个生成树。当图中的边具有权值时,总会有一个生成树的边的权值之和小于或者等于其它生成树的边的权值之和。
Kruskal算法
不停地循环,每一次都寻找两个顶点,这两个顶点不在同一个真子集里,且边上的权值最小。
把找到的这两个顶点联合起来。
初始时,每个顶点各自属于自己的子集合,共n个子集合。
每一步操作,都会将两个子集合融合成一个,进而减少一个子集合。
结束时,所有的顶点都在同一个子集合里,这个子集合就是最小生成树。
贪心策略
贪心策略就是,每次都选择权重最小的但未形成环路的边加入到生成树中。
算法图解
初始化 将图G的边集E中的所有边按权值从小到大排序,初始化节点状态,一个节点对应一个连通分支,如下图所示,同时初始化最小生成树TE = {}。
找最小 在边集E中选择最小的边e1(2, 7), 权值为1。
合并 节点2和节点7属于两个不同的连通分支,因此可以将边(2, 7)加入边集TE,执行合并操作后将两个连通分支合并成一个连通分支,同时我们规定连通分支号码小的取代连通分支号码大的,如下图所示。
代码
#include<bits/stdc++.h>
using namespace std;
int n, m;
int ans = 0;
int k = 0;
int fat[200010];
struct node
{
int from, to, dis;
}
edge[200010];
bool cmp(node a, node b)
{
return a.dis < b.dis;
}
int father(int x)
{
if (fat[x] != x)
return father(fat[x]);
else return x;
}
void unionn(int x, int y)
{
fat[father(y)] = father(x);
}
int main() {
cin >> m >> n;
for (int i = 1; i <= m; i++) {
cin >> edge[i].from >> edge[i].to >> edge[i].dis;
}
for (int i = 1; i <= n; i++)
{
fat[i] = i;
}
sort(edge + 1, edge + 1 + m, cmp);
for (int i = 1; i <= m; i++) {
if (k == n - 1) break;
if (father(edge[i].from) != father(edge[i].to))
{
unionn(edge[i].from, edge[i].to);
ans += edge[i].dis;
k++;
}
}
cout << ans;
return 0;
}
第3关:Huffman 编码
相关知识
为了完成本关任务,你需要掌握:
c++基础;
二叉树;
最小生成树概念
哈夫曼(Huffman)编码算法是基于二叉树构建编码压缩结构的,它是数据压缩中经典的一种算法。算法根据文本字符出现的频率,重新对字符进行编码。因为为了缩短编码的长度,我们自然希望频率越高的词,编码越短,这样最终才能最大化压缩存储文本数据的空间。
假设现在我们要对下面这句歌词“we will we will r u”进行压缩。我们可以想象,如果是使用ASCII码对这句话编码结果则为:119 101 32 119 105 108 108 32 119 101 32 119 105 108 108 32 114 32 117(十进制表示)。我们可以看出需要19个字节,也就是至少需要152位的内存空间去存储这些数据。
很显然直接ASCII码编码是很浪费空间的,Unicode就更不用说了,下面我们先来统计一下这句话中每个字符出现的频率。如下表,按频率高低已排序:
代码
#include<bits/stdc++.h>
using namespace std;
typedef struct //哈夫曼树的存储表示
{
char s;
int weight; //权值
int parent, lChild, rChild; //双亲及左右孩子的下标
}
HTNode, * HuffmanTree;
void Select(HuffmanTree hT, int n, int & s1, int & s2) //选择两个最小节点
{
s1 = s2 = 0;
int i;
for (i = 1; i < n; ++i) //选择两个未有双亲的节点
{
if (hT[i].parent == 0) {
if (0 == s1) {
s1 = i;
} else {
s2 = i;
break; //后面一个for循环的标记
}
}
}
if (hT[s1].weight > hT[s2].weight) //确保s1>s2
{
int t = s1;
s1 = s2;
s2 = t;
}
for (i += 1; i < n; ++i) //选择s2即break 故从i+1开始选择两个最小节点
{
if (hT[i].parent == 0) {
if (hT[i].weight < hT[s1].weight) {
s2 = s1;
s1 = i;
} else if (hT[i].weight < hT[s2].weight) {
s2 = i;
}
}
}
//cout<<s1<<" "<<s2<<"**"<<endl;
}
void CreateHufmanTree(HuffmanTree & hT) //构造哈夫曼树
{
int n, m;
cin >> n;
m = 2 * n - 1;
hT = new HTNode[m + 1];
hT[0].weight = m; // 0号节点用来记录节点数量
for (int i = 1; i <= m; ++i) {
hT[i].parent = hT[i].lChild = hT[i].rChild = 0; //初始化
}
for (int i = 1; i <= n; ++i) {
cin >> hT[i].s >> hT[i].weight; // 输入权值
}
for (int i = n + 1; i <= m; ++i) //建立过程
{
int s1, s2;
Select(hT, i, s1, s2);
hT[s1].parent = hT[s2].parent = i;
hT[i].lChild = s1;
hT[i].rChild = s2; //作为新节点的孩子
hT[i].weight = hT[s1].weight + hT[s2].weight; //新节点为左右孩子节点权值之和
}
}
int HuffmanTreeWPL_(HuffmanTree hT, int i, int deepth) //递归计算WPL
{
if (hT[i].lChild == 0 && hT[i].rChild == 0) {
return hT[i].weight * deepth;
} else {
return HuffmanTreeWPL_(hT, hT[i].lChild, deepth + 1) + HuffmanTreeWPL_(hT, hT[i].rChild, deepth + 1);
}
}
int HuffmanTreeWPL(HuffmanTree hT) //计算WPL
{
return HuffmanTreeWPL_(hT, hT[0].weight, 0);
}
void DestoryHuffmanTree(HuffmanTree & hT) //销毁哈夫曼树
{
delete[] hT;
hT = NULL;
}
int main() {
HuffmanTree hT;
CreateHufmanTree(hT);
cout << HuffmanTreeWPL(hT) << endl;
DestoryHuffmanTree(hT);
return 0;
}
第4关:单源点最短路径
相关知识
为了完成本关任务,你需要掌握:
贪心法;
Dijkstra算法;
单源点最短路径
给定一个带权有向图G=(V,E),其中每条边的权是一个实数。另外,还给定V中的一个顶点,称为源。要计算从源到其他所有各顶点的最短路径长度。这里的长度就是指路上各边权之和。这个问题通常称为单源最短路径问题。
Dijkstra算法
解决单源最短路径问题的方法之一就是Dijkstra算法。Dijkstra算法会生成一颗最短路径树,树的根为起始顶点s, 树的分支为从顶点s到图G中所有其他顶点的最短路径。此算法要求图中的所有权值均为非负数。与Prim算法类似,Dijkstra算法也采用贪心算法,它总是将当前看起来最近的最短的边加入最短路径中。
从根本上来说,Dijkstra算法通过选择一个顶点,并不断探测与之相关的边,类似广度优先搜索,找出当前距离最近的点。
图片说明
S集合最初为空,然后选取源点0,S集合为 {0},源点到其它所有点的距离为 {0, INF, INF, INF, INF, INF, INF, INF} 。图中蓝色表示 SPT,迭代的过程如下:
最终得到 SPT(最短路径树) 如下:
代码
#include<iostream>
using namespace std;
#define N 100
#define Min(a,b) a>b?b:a
#define INF 1000
int dis[N], bj[N];
int mp[N][N];
int n;
void djsk(int v, char** names)
{
int i, j, k, min;
for (i = 0; i < n; i++)
dis[i] = mp[v][i];
dis[v] = 0;
bj[v] = 1;// 标记 已找到短路
cout << names[0] << " " << dis[0] << endl;
for (i = 0; i < n; i++)
{
min = INF; k = 0;
for (j = 0; j < n; j++)//从未找到最短路径元素中找一个路径最短的
if (!bj[j] && dis[j] < min) { min = dis[j], k = j; }
if (k != 0)
cout << names[k] << " " << dis[k] << endl;
bj[k] = 1;// 标记 已找到短路
for (j = 0; j < n; j++)
if (dis[j] > (dis[k] + mp[k][j]))dis[j] = dis[k] + mp[k][j];
}
}
int main(){
/********** Begin **********/
cin >> n;
char** names = new char* [5];
char from[3], to[3];
int len;
for (int i = 0; i < 5; i++) {
names[i] = new char[5];
names[i][0] = 'v';
names[i][1] = (i + 1) + '0';
names[i][2] = 0;
dis[i] = INF;
for (int j = 0; j < 5; j++) {
mp[i][j] = INF;
}
}
fflush(stdin);
for (int i = 0; i < n; i++) {
cin >> from;
cin >> to;
cin >> len;
mp[from[1] - '0' - 1][to[1] - '0' - 1] = len;
mp[to[1] - '0' - 1][from[1] - '0' - 1] = len;
}
n = 5;
djsk(0, names);
for (int i = 0; i < 5; i++) {
delete[] names[i];
}
delete[] names;
return 0;
}