问题:有n幢建筑,其高度分别为H1,...,Hn,其宽度为1,且这些建筑紧靠在一起,当前需要在这些建筑上刷一个非常大的矩形广告,求矩形广告的最大值。
先翻译成数学题,给定n个正数的序列,定义区间值A(i,j)= Min(Hi,Hi+1,..,Hj) * (j-i+1),求所有区间值的最大值。这个问题网上都有答案,无奈答案一律都相当简单,个人实在无法理解,故成此文!
(1) 最简单的方法
直接查看所有区间的区间值,复杂度为O(n^3),如下为示例代码:
static int get_min(int *s, int start, int end) { int i, min = s[start]; for (i = start;i <= end;i++) { if (min > s[i]) min = s[i]; } return min; } int continous_sum_o3(int *s, int n) { int i, j; int min, max = s[0]; for (i = 0;i <= n - 1;i++) { for (j = i;j <= n - 1;j++) { min = get_min(s, i, j); if (max < min * (j - i + 1)) max = min * (j - i + 1); } } return max; }
(2) 使用O(1)求区间最小数
#define MIN(x, y) (((x)<(y)) ? (x) : (y)) int continous_sum_o2(int *s, int n) { int i, j; int min, max = s[0]; for (i = 0;i <= n - 1;i++) { min = s[i]; if (max < s[i]) max = s[i]; for (j = i + 1;j <= n - 1;j++) { min = MIN(min, s[j]); if (max < min * (j - i + 1)) max = min * (j - i + 1); } } return max; }
(3) 使用单调队列
unsigned int continous_sum_pq(int *s, int n) { int i, max, val; int start = 0, end = -1; int *rn = NULL, *ln = NULL, *P = NULL; rn = malloc(sizeof(unsigned int) * n); ln = malloc(sizeof(unsigned int) * n); P = malloc(sizeof(unsigned int) * n); /* calc right continuous block */ for (i = 0;i < n;i++) { for (;end >= start && s[i] < s[P[end]];end--) { rn[P[end]] = i; } end++; P[end] = i; } for (i = start;i <= end;i++) { rn[P[i]] = n; } /* calc right continuous block */ for (i = n - 1;i >= 0;i--) { for (;end >= start && s[i] < s[P[end]];end--) { ln[P[end]] = i; } end++; P[end] = i; } for (i = start;i <= end;i++) { ln[P[i]] = -1; } /* determin the max block */ max = s[0]; for (i = 0;i < n;i++) { val = s[i] * (rn[i] - ln[i] - 1); if (val > max) max = val; } #ifdef DEBUG printf("the right continous block is\n"); for (i = 0;i < n;i++) { printf("%d ", rn[i]); } printf("\n"); printf("the left continous block is\n"); for (i = 0;i < n;i++) { printf("%d ", ln[i]); } printf("\n"); #endif return max; }
区间最小数肯定是区间内的某个数,考察某一个数往左、右两个方向可延伸的最大长度,如此可以求得以这个数作为区间最小数的区间值。
往右延伸,即从此数开始,右边的若干连续数均大于等于当前数,找到这个延伸的最大长度,注意代码中当某个数出单调队列时才计算此数的延伸长度。
求得往左、往右延伸的长度,再相加即得此数作为区间最小数时的区间值
代码细节:
使用数组rn 存储某个数向右扩展的最大下标,即对于某个数H[i]来说,rn[i] 存储的是一个数组下标,从i到rn[i](不含rnr[i])的所有数均大于等于H[i]。
数组P 存储原数组的下标,对于任意的 i<j,且i,j均属于P,则有H[i] <= H[j],即P是一个单调队列。每次检查一个数H[i]时,将此数于P数组中的比较,从队尾开始比较,将所有大于H[i]的数出队列,并计算出队列元素的延伸长度,即rn值。检查完原数组中所有元素时,将数组P 中的剩余元素的rn值均设置为n,即P中剩余的所有元素均小于等于其后的所有元素