“目光短浅”
选取最优的量度标准实为贪心法的核心问题。在确定了量度标准后,每一次迭代仅考虑对当前状态最有利的步骤或者是选择。另外,每一次迭代的结果或者状态不能对前一次的迭代结果产生影响。
注:因为可以使用贪心算法的情况很少,所以需要先用数学的方法证明使用贪心法的正确性。
背包问题
问题描述:有一个容量固定为“M”的背包,另有n种需消耗不同容量的物品所需消耗容量可用“wi”表示,且当物品i放入背包后可以产生收益“pi”。现在仅装一次,每一种物品可以全部放入,也可以仅放入一部分,或者不放入,怎样装入才能使收益最大?
约束条件:装入物品所占据的容量总和不可超过“M”。
算法分析:
在此问题中明显的度量标准有两个,分别为“容量M”和“收益P”。单独考虑其中任意一种都不能得到最优解,因为当我希望“装尽可能多的物品”时,可能会发现装满一背包的物品所得收益还不如装一个消耗容量比较多的物品所获得的收益;当我希望“装收益尽可能大的物品”时,可能得到一个正好相反的结果。
以上两种度量标准都仅考虑单一方面的问题,并没有综合考虑物品对容量的消耗速度与其获益的增速,因此设计另一种度量标准,即“单位容量收益(P/M)”。每次迭代仅考虑是否有足够的容量来容纳当前“单位容量收益”最大的物品。
typedef struct _GK_NODE{
int id; // 物品ID
int weight; // 物品消耗容量
int profit; // 物品获得收益
float pwRate; // 物品单位容量收益
float load; // 物品装入容器状态
}_gkNode;
float* _GK( int *p, int *w, int m, int n )
{
// “p” 物品收益数组
// “w” 物品容量数组
// “m” 背包容量
// “n” 物品种数
int remain,i; // “remain” 当前背包剩余容量
float *x = MALLOC(float,n+1); // 解向量
_gkNode *gk = _GK_CREATE(w,p,n); // 创建物品数组且按pwRate值降序排序
for( remain=m,i=1; i<=n; i++ ) {
if( (gk+i)->weight < remain ) {
(gk+i)->load = 1;
remain -= (gk+i)->weight;
}
else break;
}
(gk+i)->load = (float)remain/(float)(gk+i)->weight;
*x = i;
for( i=1; i<=n; i++ ) {
*(x+(gk+i)->id) = (gk+i)->load; // 根据物品ID,构建解向量
}
return x;
}
有限期的作业排序问题
问题描述:
假定在一台机器上处理n个作业,每个作业均可在单位时间“1”内完成,另外每一个作业“Ji”都有一个截止日期“Di”。当且晋档作业Ji在其截止日期Di之前的某一个单位时间内完成才能获得收益“Pi”。求能获得最大收益的作业顺序?
算法分析:
算法初始状态是将所有作业,按照“Pi”(该问题的量度标准)降序排序。这样在逐个判断作业时,首先保证了当前正在处理的作业“Jcur”是在剩余部分中收益最大的一个。
假设在Sin中已经存在了k个作业,可以确定这k个作业应该满足如下条件:
1. D(Jin_1) ≤ D(Jin_2) ≤ … ≤ D(Jin_k)
2. D(Jin_j) ≥ Jin_j, j ∈ [1,k]
那么若要插入Jcur同样需要满足该条件:
1. D(Jin_1) ≤ D(Jin_2) ≤ … ≤ D(Jcur) ≤ … ≤ D(Jin_k+1)
2. D(Jin_j) ≥ Jin_j, j ∈ [1,k+1]
当n个作业都处理完毕后即可得到该问题的最优解。
typedef struct _GJ_NODE {
int id; // 作业号
int profit; // 按时完成作业获得的收益
int deadline; // 作业截止日期
}_gjNode;
int* _GJ_SORT( _gjNode *gj, int n )
{
// “gj” 已将所有作业按照收益profit的降序排序
// “n” 为作业总数
// “jobSequence” 记录最优解序列
// “curJob” 当前处理作业
// “curInSeqJobNum” 当前已加入jobSequence的作业数
// “locate” 指向curJob插入位置,curJob若可加入jobSequence那么其位置应是locate+1
/* Initialization */
int locate, curInSeqJobNum, curJob, i;
int *jobSequence = MALLOC(int,n+1);
for( i=0; i<=n; i++ ) jobSequence[i] = 0;
jobSequence[1] = 1; // 降低一个作业加入解集
curInSeqJobNum = 1;
for( curJob=2; curJob<=n; curJob++ ) {
/* 确定新作业插入位置locate */
locate = curInSeqJobNum;
while( (gj+jobSequence[locate])->deadline > (gj+curJob)->deadline && \
(gj+jobSequence[locate])->deadline > locate ) {
locate = locate - 1;
}
/* 在locate之后,即locate+1位置插入i */
if( (gj+jobSequence[locate])->deadline <= (gj+curJob)->deadline && \
(gj+curJob)->deadline > locate ) {
for( i=curInSeqJobNum; i>=(locate+1); i-- ) {
jobSequence[i+1] = jobSequence[i];
}
jobSequence[locate+1] = curJob;
curInSeqJobNum++;
}
}
/* 将jobSequence内的值替换成真正的ID号 */
for(i=0; i<=curInSeqJobNum; i++) {
jobSequence[i] = (gj+jobSequence[i])->id;
}
return jobSequence;
}
哈弗曼编码(Huffman Coding)
算法就不多说了,必学内容,直接上伪代码(摘自《计算机算法基础》第三版,徐祥宣/崔国华/邹海明,华中科技大学出版社):
line procedure TREE( L, n )
for i←1 to n-1 do
call GETNODE(T) // 生成新节点T,用于归并两棵树
LCHILD(T) ← LEAST(L) // LEAST:在L中取权重最小的树作为T的左孩子,并将该树从L中移除
RCHILD(T) ← LEAST(L)
WEIGHT(T) ← WEIGHT(LCHILD(T))+WEIGHT(RCHILD(T))
call INSERT(T)
repeat
return(LEAST(L)) // 留在L中的树是归并树
end TREE
下面是自己写的一段C代码:
typedef struct _BITREE_NODE {
int data;
struct _BITREE_NODE *lChild,*rChild;
struct _BITREE_NODE *next; // Used to creating huffman tree
}_biTreeNode;
typedef struct _BITREE {
_biTreeNode *root;
int buf; // Point to some operation's result
int elemNum;
}_biTree;
_biTreeNode* createBiTreeNode(int newData); // 实现部分略
_biTree* createBiTree(); // 实现部分略
/* Huffman Tree */
_biTree* _HUFFMAN_TREE( int *l, int n)
{
int i;
_biTree *bt = createBiTree();
_biTreeNode *head = createBiTreeNode(0); // 以链表的形势存放树根节点,data存放链表中节点数
_biTreeNode *p1,*p2; // 遍历指针控制借点的添加以及删除
_biTreeNode *tmpBiNode;
if( n == 1 ) {
tmpBiNode = createBiTreeNode(l[1]);
bt->root = tmpBiNode;
bt->elemNum = 1;
return bt;
}
QUICKSORT(l,n); // 先将整数数组按权重(data值)升序排序,这样每次迭代仅用取前两个即可。(注①)
/* Initialization */
/* 以链表的形势表示树的集合 */
p1 = head;
for( i=1; i<=n; i++ ) {
tmpBiNode = createBiTreeNode(l[i]);
p1->next = tmpBiNode;
head->data++;
p1 = p1->next;
}
bt->elemNum = n;
/* Huffman Tree Algorithm */
do {
/* 每次迭代只用将前两个合并成棵新树 */
p1 = head->next;
p2 = p1->next;
tmpBiNode = createBiTreeNode( p1->data + p2->data );
tmpBiNode->lChild = p1;
tmpBiNode->rChild = p2;
head->next = p2->next; // 将p1、p2所指向的元素从链表中删除
/* 将生成的新树按照根节点tmpBiNode的权重以升序插入进链表中 */
p1 = head;
while( p1->next != null_ptr ) {
if( tmpBiNode->data <= p1->next->data ) break;
p1 = p1->next;
}
tmpBiNode->next = p1->next;
p1->next = tmpBiNode;
head->data--;
bt->elemNum++;
} while( head->data > 1 ); /* 当链表中元素的个数等于1时即跳出循环 */
bt->root = head->next;
head->next = null_ptr;
free(head);
return bt;
}
注①:QUICKSORT(l,n);
的实现参考我的另一篇博客《Algorithm理解用例:分治法》(http://blog.csdn.net/credolhcw/article/details/54773266)
Prim
无向图求解最小生成树问题中常用的方法之一就是Prim算法。其核心思想就是:每迭代一次,就将距已选节点集合最近的一个节点加入到集合中,直到求出最小生成树为止。
如下图所示用例:
摘自:《计算机算法基础》第三版,徐祥宣/崔国华/邹海明,华中科技大学出版社,P114
图的存储,我采用的是矩阵的方式,代码如下:
...
int cmpEdgeDataG( int p, int q)
{
// 考虑到以后可能会碰到边值为负的情况,所以先这么写着
#ifdef GRAPH_DATA_UNSIGNED
if( p == q ) return 0;
else if( p == GRAPHDATA_MAXVALUE ) return 1; // p>q, p=+∞
else if( q == GRAPHDATA_MAXVALUE ) return -1; // p<q, q==+∞
else if( p > q ) return 1;
else return -1;
#endif
}
...
int** PRIM( _graph g )
{
/*
* 返回最小生成树的边集T[n][2]
* 有n个节点的图的最小生成树的边数是n-1
* T[0][0]: 最小总代价
*/
int **T,i,j,n = g[0][0].data; // n表示节点数
/*
* nearNode[A] = B 语义为:
* A为待选节点,并不在当前生成树中
* B为生成树中所有节点集S中的一个节点
* 边(A,B)是A到S中所有节点的别种最短的一条
*/
int *nearNode = MALLOC(int,n+1);
/*
* 初始化T
*/
T = MALLOC(int*,n);
for( i=0; i<n; i++ ) T[i] = MALLOC(int,2);
/*
* 将最短边(A,B)存入(T[1][0],T[1][1])
* 同时将当前生成树的最小代价存入T[0][0]
*/
T[0][0] = GRAPHDATA_MAXVALUE;
for( i=2; i<=n; i++ ) {
for( j=1; j<i; j++ ) {
if( cmpEdgeDataG(g[i][j].data,T[0][0]) == -1 ) {
T[0][0] = g[i][j].data;
T[1][0] = i;
T[1][1] = j;
}
}
}
/*
* 根据最短边的两个节点A,B初始化数组nearNode
*/
for( i=1; i<=n; i++ ) {
if( cmpEdgeDataG( g[T[1][0]][i].data, g[i][T[1][1]].data) == -1 ) { // edge(A,i) < edge(i,B)
nearNode[i] = T[1][0];
}
else {
nearNode[i] = T[1][1];
}
}
nearNode[T[1][0]] = nearNode[T[1][1]] = 0; // 已选择的节点后续不考虑
/*
* 找T中其余的n-2条边
*/
for( i=2; i<=n-1; i++ ) {
// 找出(j,nearNode[j])最小的边
T[0][1] = GRAPHDATA_MAXVALUE; // T[0][1]: 缓存最小值
for( j=1; j<=n; j++ ) {
if( nearNode[j] == 0 ) continue;
if( cmpEdgeDataG( g[j][nearNode[j]].data, T[0][1] ) == -1 ) {
nearNode[0] = j; // 缓存最小值对应的节点号
T[0][1] = g[j][nearNode[j]].data;
}
}
// 将找出的边加入T,并更新总代价
T[i][0] = nearNode[0];
T[i][1] = nearNode[nearNode[0]];
T[0][0] += T[0][1];
// 更新nearNode
nearNode[nearNode[0]] = 0; // 刚选入的节点不考虑
for( j=1; j<=n; j++ ) {
if( nearNode[j] == 0 ) continue;
if( cmpEdgeDataG( g[j][nearNode[0]].data, g[j][nearNode[j]].data ) == -1 ) {
nearNode[j] = nearNode[0];
}
}
}
return T;
}
Kruskal
另一种常用的求解无向图最小生成树的方法就是Kruskal算法。
算法思想:
每次迭代选取当前代价最小的边,若不会与已选的边构成环即将该边加入到已选边集合中,否则抛弃该边。
为了能够尽快确定是否成环,我采用堆的方法记录各节点之间的关系,并且用数组来存储堆:
①、以节点id为下标,其内容表示该节点的父节点。
②、各堆的根节点用负数表示堆内的节点数。
图示用例:
摘自:《计算机算法基础》第三版,徐祥宣/崔国华/邹海明,华中科技大学出版社,P116
代码如下:
int combineNodeKruskal(int *nodeList, int s, int e)
{
while(nodeList[s]>0) s = nodeList[s]; // 定位节点s所在堆的根节点
while(nodeList[e]>0) e = nodeList[e]; // 定位节点e所在堆的根节点
if( s == e ) return 1; // 会成环,返回1
// 合并堆,将节点数少的树作为子树
if( nodeList[s] >= nodeList[e] ) {
nodeList[e] += nodeList[s];
nodeList[s] = e;
}
else {
nodeList[s] += nodeList[e];
nodeList[e] = s;
}
return 0;
}
int** KRUSKAL( _graph g )
{
/*
* 返回最小生成树的边集T[n][2]
* 有n个节点的图的最小生成树的边数是n-1
* T[0][0]: 最小总代价
*/
int **T,i,n = g[0][0].data; // n表示节点数
_enkNode *p,*edgeList = createEdgeListKruskal(g); // 将所有的边依代价升序排序,存入链表edgeList中
int *nodeList = MALLOC(int,n+1); // 建立节点堆
for( i=0; i<=n; i++ ) nodeList[i] = -1; // 初始化节点堆
/*
* 初始化T
*/
T = MALLOC(int*,n);
for( i=0; i<n; i++ ) T[i] = MALLOC(int,2);
/*
* 将代价最小的两条边加入T中,并从edgeList中删去此边
*/
p = edgeList->next;
delEnkNodeKruskal(edgeList,p);
combineNodeKruskal(nodeList,p->startNode,p->endNode); // 更新节点堆
T[1][0] = p->startNode;
T[1][1] = p->endNode;
T[0][0] += p->data;
p = edgeList->next;
delEnkNodeKruskal(edgeList,p);
combineNodeKruskal(nodeList,p->startNode,p->endNode);
T[2][0] = p->startNode;
T[2][1] = p->endNode;
T[0][0] += p->data;
/*
* 构造节点堆
*/
i=3; // 可以直接从3开始,因为考虑第3条边时才有可能出现环
while( i <= n-1 ) {
p = edgeList->next; // 从edgeList中选取最小成本的边p
delEnkNodeKruskal(edgeList,p); // 将p总edgeList中删去
if( combineNodeKruskal(nodeList,p->startNode,p->endNode) == 0 ) {
T[i][0] = p->startNode;
T[i][1] = p->endNode;
T[0][0] += p->data;
i++;
}
}
return T;
}
Dijkstra
单源点最短路径问题,常用的方法之一就是Dijkstra算法。
算法思想:
总共有n个结点,经过第i-1次迭代,确定了结点Vi-1,使得路径(V0,…,Vi-1)在源点V0到所有未被选中的结点的路径中的值最小。
图例:
摘自:《计算机算法基础》第三版,徐祥宣/崔国华/邹海明,华中科技大学出版社,P121-122
因为可以确定最终得到的图其实是一个以结点V0为根的最小生成树,所以我参考上文的方法用堆的方式进行路径信息的存储。
代码如下:
...
int addEdgeDataG( int p, int q)
{
#ifdef GRAPH_DATA_UNSIGNED
if( p == GRAPHDATA_MAXVALUE || q == GRAPHDATA_MAXVALUE ) return GRAPHDATA_MAXVALUE;
else return p+q;
#endif
}
...
int* DIJKSTRA( _graph g, int tNode)
{
/*
* tNode为源点
*/
int i,n = g[0][0].data;
int *minCost = MALLOC(int,n+1); // 最小代价数组
int *nodeHeap = MALLOC(int,n+1); // 节点堆
/*
* 初始化最小代价数组minCost[i] 表示当前节点tNode到i的最短路劲长度
* 初始化节点堆: nodeHeap[i] 表示节点i的父节点,值为负时表示为根节点
*/
minCost[0] = GRAPHDATA_MAXVALUE;
for( i=1; i<=n; i++ ) {
minCost[i] = g[tNode][i].data;
if( cmpEdgeDataG(minCost[i],GRAPHDATA_MAXVALUE) == -1 ) {
nodeHeap[i] = tNode;
if( cmpEdgeDataG(minCost[i],minCost[0]) == -1 && minCost[i] > 0) { // 定位最短路径及长度
minCost[0] = minCost[i];
nodeHeap[0] = i;
}
}
else nodeHeap[i] = 0; // 表示暂时还没确定父节点
}
nodeHeap[tNode] = -1; // tNode为根节点
/*
* 每迭代一次确定一个最小路径
*/
do {
minCost[nodeHeap[0]] *= -1; // 确定新选中的节点
nodeHeap[tNode]--; // 树中节点数加1
/*
* 更新minCost和nodeHeap
*/
for( i=1; i<=n; i++ ) {
if( minCost[i] <= 0 && minCost[i] != GRAPHDATA_MAXVALUE ) continue; // 已选中的节点不用更新,GRAPHDATA_MAXVALUE可能值为-1
if( cmpEdgeDataG(addEdgeDataG(minCost[0],g[nodeHeap[0]][i].data),minCost[i]) == -1 ) {
// (minCost[0] + cost<nodeHeap[0],i>) < (minCost[i])
minCost[i] = addEdgeDataG(minCost[0],g[nodeHeap[0]][i].data);
nodeHeap[i] = nodeHeap[0];
}
}
// 更新minCost[0]和nodeHeap[0]
minCost[0] = GRAPHDATA_MAXVALUE;
for( i=1; i<=n; i++ ) {
if( minCost[i] <= 0 ) continue;
if( cmpEdgeDataG(minCost[i],minCost[0]) == -1 ) {
minCost[0] = minCost[i];
nodeHeap[0] = i;
}
}
} while( nodeHeap[tNode] > -n ); // nodeHeap[tNode]以负数的形势记录树中节点数
free(minCost);
return nodeHeap;
}