切割篱笆问题
假设有道篱笆用N个同宽的木条拼接而成。因年久失修,有些木板已经折断,因而整个篱笆呈现出参差不齐的轮廓,所以要用新的木板替换。不过为了环保,可以用一部分旧篱笆切割出长方形的木板充当木料。图(b)表示在(a)形状的篱笆中能切割出的最大长方形。给定构成篱笆的各个木板的高度,编写程序计算能够切割出的最大长方形面积。不能斜线切割,即不允许采用如图(c)的切割方法。
图1 切割篱笆问题
暴力算法
给出保存各个木板高度的数组h[],截取第l个木板到第r个木板的长方形面积可用如下公式表示:
最简单的解法就是,用双重for语句把可能的l和r值都带入上述公式,求出最终答案。于是可以得到一个时间复杂度的暴力算法。
// 给定保存木板高度的数组h[]时,返回长方形的最大宽度。
int bruteforce(const vector<int>& h){
int ret = 0;
int N = h.size();
// 检索所有可能的left、right组合
for(int left = 0; left < N; ++left){
int minHeight = h[left];
for(int right = left; right < N; ++right){
minHeight =min(minHeight, h[right]);
ret = max(ret, (right-left+1)*minHeight);
}
}
return ret;
}
分治算法
为设计出分治算法,首先要确定以何种方式分割给定的问题。如图所示,先把n个木板平均分成两个子问题。那么我们期望的长方形会符合以下三种可能性之一:
- 最大面积的长方形只能在左侧的子问题中获得
- 最大面积的长方形只能在右侧的子问题中获得
- 最大面积的长方形横跨左右两侧的子问题
图2 切割篱笆问题的分治算法
对于前两种情况,只要递归调用一半子问题即可得到答案。然后选取较大值即可。之后再找出快速解决第3种情况的方法,就可以完成对此问题的分治算法。
横跨左右两侧子问题的解法
怎样才能找出横跨左右两侧子问题的面积最大的长方形呢?
首先我们需要明白一个事实:该长方形必定横跨两个子问题边界的两个木板。假设从这个长方形开始分别向左右两侧一格一格扩展下去,即可向左移动一格,也可向右移动一格,那么应该选择图2(c)中虚线表示的哪个长方形呢?
正确答案当然是:选择包含更高木板的右侧长方形。图(3)表示这种寻找方向。
图3 切割篱笆问题中合并算法的操作过程
于是我们得到切割篱笆问题的分治算法如下:
//保存各个木板高度的数组
vector<int> h;
//返回h[left..right]区间中可截取的面积最大长方形的宽度。
int solve(int left, int right){
// 初始部分:只有一个木板的情况
if(left==right) return h[left];
//分割为[left, mid]和[mid + 1, right]两个区间的子问题
int mid = (left + mid) / 2;
// 分别计算两个子问题。
int ret = max(solve(left, mid), solve(mid + 1, right));
// 子问题3:找出横跨两个子问题的面积最大的长方形
int l0 = mid, hi = mid;
int height = min(h[l0], h[hi]);
//只考虑包含[mid, mid+1]的两个长方形
ret = max(ret, height * 2);
// 扩展长方形直到覆盖所有输入值。
while(left< l0 || hi < right){
// 总是向高度更高的方向扩展
if(hi < right && (l0 == left || h[l0-1] < h[hi+1])){
++hi;
height = min(height, h[hi]);
}
else{
--l0;
height = min(height, h[l0]);
}
//扩展后的长方形宽度
ret = max(ret, height*(hi -l0 + 1));
}
return ret;
}