习题16.1-4 区间图着色问题。边最多的顶点G,其边数n即需要的颜色数,也即教室数。有n种颜色,顶点标为第i种颜色,则此活动可放在第i个教室举行。从一顶点开始广度优先遍历,将相连接的点标为不同颜色,标颜色同时检查与此相连的所有点颜色不同。然后检查全部顶点,有未标色的顶点,按照上法重新开始标色。
习题16.1-5 如果每个活动有值vk,要求vk总和的最大值,那么这个题就需要用动态规划求解了。时间为i,若有一个完成的活动,此活动编号为j,则D[i]=max( D[i- tj] + v[j] , D[i-1] ),表格D记录了到时间i为止最佳的选择方案。
习题16.2-1 证明分数背包具有贪心性质。考察最大vi/wi的商品,在背包的部分空间wi中,无论用什么其他商品代替,都只有此i商品具有最大价值,同时剩余的w-wi空间,无论如何选择都不会影响到这一wi的部分。此部分只能选择i商品。综上,背包的解法具有贪心性质。
习题16.2-2 动态规划0-1背包问题。V[i, j]代表第i次选择商品,j是在背包重量为j时的最佳方案。
for i = 1 to n
V[i, j]=max( V[i-1, j-wi] + vi , V[i-1, j] ) //两种选择,放入第i个商品或者不放入
best_result=max(V[n, w])
习题16.2-4 从起点开始,最大取水方案是每次取水后跑尽可能远。证明:若取水方案不包括第一次开始时尽可能远的点a1,假设第一个取水点为a1‘,那么a1'必然在a1之前,那么从a1到an的取水次数大于a1'到an的取水次数。距离远反而次数少,这是不可能的。
习题16.2-6 分数背包问题主要是排序,O(n)的排序算法可以用基数排序。
习题16.3-7 3进制的赫夫曼编码。与2进制类似,选择3个有最小概率的元素,组成子树,将子树与其他未排序的点一起,继续组成3分支的子树。
for i =1 to n-1
allocate a new node A
A.left=x=min_freq(Q)
A.middle=y=min_freq(Q)
A.right=z=min_freq(Q)
A.freq=x.freq+y.freq+z.freq
insert A to Q
习题16.3-8 因为最高频率不超过最低频率的2倍,所以不存在最低2个元素频率相加大于最高1个的情况。那么层层传递上去,最低2个频率相加始终不能超过最高的1个。最终所有的元素都在同一层。
思考题16-1 贪心法解找零问题。首先用最大面额,直到不能用为止,然后换次大面额,依此类推……直至找到全部零钱。
贪心法能或不能得到最佳解,与面额有关。1,面额为C^k形式的硬币,因为C[i]^ki=C[i-1]^kj,即任意小面额的硬币总能将一部分换成更少的大面额的硬币,那么只要大面额的硬币尽可能多,那么一定能得到更少的硬币数量。2,比如若出现10, 9, 1的面额,那么贪心法不能保证得到最佳解。
任何k种不同面额的情况,需要动态规划。D[i]=D[i-Cj]+Cj表示硬币得到总额为i的最佳方案。
for i = 1 to n
D[i]=i;
for(i=1; i!=n;i++){
for j= 1 to k
D[i]=min( D[i-C[j]]+C[j], D[i]);
}
对于可以抢占的情况,将时间最短的任务分配最高的优先级,优先级高的任务一旦满足释放时间,立即抢占优先级低的任务。
思考题16-3 列向量都是有2个坐标为1,其余坐标为0,代表从一个顶点到另一个顶点。若有2个列向量的1个坐标相同,则表明2边有共同顶点。形成环的V1,V2..Vk相加后,所有的公共点上坐标都是2的整数倍,模2后全部坐标为0,可知环的所有边的列向量线性相关,无环的所有边列向量线性无关。
因为矩阵的秩即最大列向量线性无关组的大小,所以最大无环边集的边总数是一定的。那么就用贪心算法,每次把最大的权重加入可得。
有向图可以在列向量的坐标上加上+-号标明边是从顶点出发还是到顶点终止。两个列向量如果有一个坐标相加为0,则它们首尾相连。形成环的V1,V2..Vk相加后,所有的公共点上坐标都是0,可知有向图的环的所有边的列向量线性相关,无环的所有边列向量线性无关。
思考题16-4 取有最大惩罚值wi的任务i与任意其他任务j互换即可证明。
思考题16-5 离线缓存问题,将来最远的贪心策略。假设将来最远的元素为wi,若不是换出wi而是前面的wj,则运行到wj的位置,必有一次换出任意元素,换入wj,此后的调度与当初直接换出wi是一致的。证明当初不换入wi,使得总交换次数多了一次。可知将来最远策略是最优的。
实现此管理功能,需要维护一个hash_map<>variable_table保存所有元素的信息,一个vector<>cache_table保存缓存各槽的信息,每次遇到一个元素,判断是否在缓存内,不在则找到缓存内各元素的距离最大值将该元素换出。每个元素的信息放在一个variable对象里面,每个槽的信息放在cache_slot对象里面。
class Variable{
string name;
int cache_slot; //在缓存中位置,或者NIL表示不在缓存中
int future_dist; //序列中下次出现此元素的距离
listpositions; //这个list用来放置此元素在序列中每次出现的位置。但此list可有可无,详情见后文。
int current_p; //结合上表,表示元素在list已出现p次,可以帮助快速找到元素下一次出现位置
}
class Cache_Slot{
int slot_num;
Variable v;
}
hash_mapvariable_table //表格放置元素信息
vectorcache_table //表格放置缓存各槽信息
while(r){
r=r.next;
current_v++; //辅助更新variable的future_dist
in_v=variable_table[r.name];
if(in_v.cache==NIL){
out_v=max_future_dist( cache_table ){ for each s in cache_table{v=max(v, s.v); } return v; }();
cache_table.exchange(out_v, in_v); //执行换入换出,更新cache_table
out_v.update_cache();
in_v.update_cache();
}
for each slot in cache_table{
v=variable_table[slot.name];
v.update_dist(); //这里用的方法是在Variable的list里面直接查找,如果Variable中不要list,可以直接在序列中向后查找
}
注:max_future_dist()是O(k)的时间复杂度。k个槽中的元素更新最远距离的复杂度依赖于实现方式。如果每次都是重新向前搜索一遍,并用set保存搜索的元素,时间复杂度是O(n*lg(k)),空间复杂度是O(k),如果用list记录每个元素的所有出现位置,时间复杂度是O(k),但是空间复杂度是O(n)。如果是对静态的序列,可以预见不同元素的总数,用第二种方法较好;但如果对于动态序列边输入边处理的情形,因为元素总数不确定,前面出现过的元素都必须保留,会导致n越来越大,list越来越多,也许用第一种方法更好。