By yejinru
1. 给定一个数组A,任务是设计一个算法求得数组中的“主元素”,即在数组中个数超过数组总元素个数一半的元素。但是数组中元素的数据类型可能是复杂类型,这意味着数组中的元素进能够比较是否相等而不存在序关系,设对于两个元素A[i]和A[j],判定是否A[i]=A[j]需要常数时间。
(a) 设计一个时间复杂性为O(n log n)的算法解此问题
(b) 设计一个时间复杂性为O(n)的算法解此问题.
A.O(n log n)采用分治的策略:
a) 由于数组存在主元素,所以如果把该数组平均分成两部分,则主元素必为两部分中至少一部分的主元素。因此我们:
b) 递归查找两部分T1,T2的主元素m1,m2,如果:
c) 递归结束返回该部分的主元素。
B.O(n)算法:参考自编程之美书上的算法。
我们同时删除两个不同的元素,不会改变剩下元素中的主元素。
因此我们可以每次删除两个不同的元素,直到不能删除为止,剩下的元素即为主元素。
2. 令I1, …, In是n个区间,其中任一区间Ii=(ai,bi),假设这些区间按照bi从小到大排序,每一个区间有一个权重vi,考虑如下两个问题:
区间安排问题 P1: 找到最大数量互不相交的区间,例如,对于四个区间I1 = (1,2); I2 = (2,3); I3 = (1,4); I4 = (4,5),一个解是{I1, I2, I4}
加权任务安排问题P2: 找一个互不相交区间的集合,使得这些区间的权重之和最大,例如I1 = (1,2), v1=0.9; I2 = (2,3), v2=0.5; I3 = (1,4), v3=4; I4 = (4,5), v4=2,解是{I3, I4}。
(a) 给出解决问题P1的线性时间算法。
(b) 给出解决问题P2的动态规划算法,要求写出递归方程和伪代码,并分析算法时间复杂性。
a) 第一次找l1。
b) 第二次时,找到最小的i,满足ai>=b1。
c) 然后每次找最靠前的与当前所选区间不重叠的区间即可。
B. dp[i] = max{dp[j]} + v[i] ,j需要满足 bj<=ai,1<=j<i ,初始化:dp[0] = 0
时间复杂度为O(n^2)
3. 考虑多背包问题,即给定n个物品,其中物品i的价格是vi, 重量是wi,有m个背包,背包j最大能装物品的重量均为Bj,求这些背包能够装下物品的最大价格,其中每个物品要么完全放入背包,要么不放入。
对于此问题一个显然的贪心算法如下:利用精确算法选择物品装第一个背包,然后移除装入第一个背包的物品,然后按此方法依次装后面的背包。证明此贪心算法不能给出精确解,在所有Bj都相等时也是如此。设计精确算法解决此问题。
反例:
物品:
重量:wi |
价格:vi |
4 |
4 |
3 |
2 |
2 |
1 |
背包:
重量:bj |
5 |
4 |
贪心选出的结果是:背包1放物品1,背包2放物品2,总价值为6
实际上最优解为:背包1放物品2,3,背包2放物品1,总价值为7
貌似是一个NP问题,没有一个多项式复杂度的解法,因此我们可以通过dfs爆搜。
伪代码:
最优解为ans。
// j表示第j个背包,has表示该背包已经装了has重量的物体,sum表示当前所有背包装的物品价值总和
dfs(j,has,sum){
if(sum>ans) // 更新答案
ans = sum;
if(j>m) // 全部背包放满
return;
for(i = 1 to n) // 遍历所有的物品
if( !use[i] && has+w[i]<=b[j] ){ // 如果物品i还没放入到背包且能放入当前背包中
use[i] = true; // 把物品i放入到背包j
dfs( j,has+w[i],sum+v[i] );
use[i] = false; // 回溯
}
dfs(j+1,0,sum); // 背包j不再放入物品,用下一个背包安放物品
}
4. 给定一个城市集合,一些城市之间由高速公路连接,这些城市和城市之间的高速公路构成了一个无向图G = (V,E),每条边e=(u,v)∈E表示一条城市u到v的高速公路,e上的权重le表示该高速公路的长度。一辆车需要从城市s到达城市t,但该车的油箱存油最多能走L公里,每个城市有一个加油站,但是城市之间没有加油站,因此,只有当le<L的时候,才能走e对应的高速公路。回答下列问题:
(a) 设计一个时间复杂性O(E)的算法,判定是否这辆车能够从城市s走到城市t。
(b) 如果准备买一辆新车,需要知道保证车从城市s成功走到城市t最少需要多少大的油箱,请设计时间复杂性为O((|V | + |E|) log |V |)的算法解决该问题。
a) bfs:
b) dfs:
伪代码: bfs版本的: bfs() { queue<int> Q; Q.push(s); use[s] = true; while( Q.empty() == false ) { x = Q.front(); Q.pop(); if(x==t) return “可达”; for( 搜索所有的边(x,y) ) { if( 边权>=L ) continue; if( use[y]==false ) { use[y] = true; Q.push(y); } } } return “不可达”; } dfs版本的: dfs(x) { if(x==t) return “可达”; for( 搜索所有的边(x,y) ) { if( 边权>=L ) continue; if( label[y] == “可达” ) { label[x] = “可达”; return “可达”; } else if( label[y]==”不可达” ) { continue; } else if( dfs(y)==”可达” ) { label[x] = “可达”; return “可达”; } else { // 搜索y时返回的是不可达 continue; } } label[x] = “不可达”; return “不可达”; }
B. O((|V | + |E|) log |V |):
用数组d[x]表示从节点s到节点x最少需要d[x]容量的油箱。
初始化:d[s] = 0 , 非s节点x:d[x] = INF;
1.把d[s]加入到平衡树中,平衡树以d[x]作为第一关键字,以x作为第二关键字。
2.从平衡树中取出最左边的元素 d[x]和x,然后遍历x的相邻节点y。
1.如果d[y],y在平衡树中,先把d[y],y从平衡树中删掉,然后再把更新后的d[y],y加入到平衡树中。
2.如果d[y],y不在平衡树中,直接把更新后的d[y],y添加到平衡树中。
3.重复以上步骤,直到从平衡树中取出的最左元素为d[t],t时,d[t]就是答案。
Ps: O((|V | + |E|) log |E |):
用数组d[x]表示从节点s到节点x最少需要d[x]容量的油箱。
初始化:d[s] = 0 , 非s节点x:d[x] = INF;
用数组use[x]表示节点x是否曾经进入到堆中。
1.把d[s]放入到最小堆中,且最小堆需要额外维护d[s]对应的节点s。
2.从最小堆中取出堆顶元素d[x]以及额外信息x。
3.如果use[x]==true,忽视。
4.否则,use[x] = true,遍历x的相邻节点y,如果d[y]比max{d[x],l(x,y)}大时,更新d[y],并把d[y]以及y放入到最小堆中。
5.重复以上步骤直到节点t出堆。
5. 考虑编辑距离的一种变形,其允许在字符串后无代价地插入无限多个字符,该编辑距离描述为:
ed'(A, B)=min{ed(A, C)|C是B的前缀}, 其中函数ed()是普通的编辑距离函数。根据要求设计算法,要求算法的时间复杂性都是O(|A||B|)
(a). 设计算法,对于给定的字符串A和B,计算ed'(A, B);
(b). 设计算法,对于给定的字符串A,B和整数k,判定是否B存在某个后缀B’,满足ed’(A, B’)≤k。
伪代码:答案为ans
for( i=0 to |A| )
dp[i][0] = i;
for( j=0 to |B| )
dp[0][j] = j;
for( i=1 to |A| )
for( j=1 to |B| )
dp[i][j] = min{
dp[i-1][j]+1,
dp[i][j-1]+1,
dp[i-1][j-1]+( A[i]!=B[j] )
};
ans = INF;
for( j=0 to |B| )
ans = min(ans,dp[ |A| ][j]);
B. 就是问题一的变形,我们只需要把A,B串翻转为A1,B1,求ed’(A1,B1),则ed’(A1,B1)即为问题所求。
reverse( A );
reverse( B );
for( i=0 to |A| )
dp[i][0] = i;
for( j=0 to |B| )
dp[0][j] = j;
for( i=1 to |A| )
for( j=1 to |B| )
dp[i][j] = min{
dp[i-1][j]+1,
dp[i][j-1]+1,
dp[i-1][j-1]+( A[i]!=B[j] )
};
for( j=0 to |B| )
ans = min(ans,dp[ |A| ][j]);
6.将一根木棒折成若干份,每折一次的代价是当前这段木棒的长度, 总代价是折这根木棒直到满足要求所需要的所有操作的代价。例如,将一根长度为10的木棒折成四段,长度分别为2, 2, 3, 3,如果先折成长度为2和8的两段,再将长度为8的折成长度为2和6的两段,最后将长度为6的折成长度为3的两段,这些操作的代价是10+8+6=24;如果先折成长度为4和6的两段,在分别将长度为4的折成长度为2的两段、长度为6的折成长度为3的两段,则这些操作的代价是10+4+6=20,比上一种方案更好一些。
该问题的输入是木棒的长度L和一些整数c1,…,cn, 要求将木棒折成长度为c1, …, cn的n段且操作代价最小,请设计动态规划算法解决该问题。
O(n^3)做法
dp[i][j] = min{ dp[i][k]+dp[k][j]+(ci+...+cj) } ( i<k<j )
边界处理:
如果i = j时,dp[i][j] = 0;
如果i +1 = j时,dp[i][j] = ci+cj
求(ci+...+cj) 时,我们需要用到前缀和相减的方式降低复杂度:
用数组sum[i]表示c1+...+ci,dp方程容易想到:sum[i] = sum[i-1]+ci
所以(ci+...+cj) = sum[j]-sum[i-1]
O(n^2)做法:
利用平行四边形法则可以判断该dp转移方程可以用斜率优化:当i,j确定时即可确定k,而不用逐一枚举k,从而把复杂度降到O(n^2)。(不作要求)
7.考虑特殊的0-1背包问题:有n个物品,每个物品i价值和重量都是wi,背包能容纳物品的最大重量是C, 选择背包能容纳的物品集合,使得这些物品价值之和最大。回答下列问题:
(1) 若物品的重量(价值)分别是1, 2, …,2n-1, 证明该0-1背包问题可以用贪心法求解并写出该贪心算法的伪代码。
(2) 请写出一个物品重量(价值)序列,使得上述贪心法无法得到最优解。
1).证明:
贪心:每次选取最大重量且满足2^k<=背包剩余重量的物品k
背包的最大容量为C,假设我们用总量为 2^k 的物品能放下,用物品2^(k+1)不能放下。如果我们不放入物品2^k,由于2^(k+1)放不下,以及2^k<=C和
( 1+2+...+2^(k-1) ) <2^k。所以我们不选重量为2^k的物品选出的价值比选择2^k的物品要少。因此贪心法求解是正确的。
贪心伪代码:
ans = 0;
for(i=n-1 down to 0)
if( 2^i <= C ){
C -= 2^i;
ans += 2^i;
}
2).
反例:
重量:wi |
价格:vi |
4 |
4 |
3 |
3 |
2 |
2 |
C为5时:
贪心选出的答案为4
正解为5