C.Skyscrapers
前置知识
线段树、单调栈、分治、一点动态规划思维。
题目链接
C1:http://codeforces.com/contest/1313/problem/C1
C2:http://codeforces.com/contest/1313/problem/C2
建议先自行阅读英文版题目,不求完全知道它在讲什么,但求了解它在问什么。
题目大意
现在要建 n 栋楼,假设第 i 栋楼建了 ans[i] 层,有两个条件:
1. 第 i 栋楼最多只能建 m[i] 层,即 1<=ans[i]<=m[i];
2. 数组 ans 对于任意 i 都需满足:不存在 j<i<k 使得 ans[i]<ans[j] && ans[i]<ans[k];
要求在满足上述条件下最终建好的总楼层数尽可能大,即 ans[1]+....+ans[n] 尽可能大,输出数组 ans。
方法一
首先要理解一个思维,假设我们当前在处理区间 [l,r] 内的建楼问题,并且数组 m 在区间 [l,r] 中的最小值出现在下标为 pos 的位置。为了满足条件 2,我们要么让 ans 数组在区间 [l,pos-1] 都等于 m[pos],要么让 ans 数组在区间 [pos+1,r] 都等于 m[pos]。
理解了上面的思维,注意到上述思维对每个区间都成立,我们一开始处理的是 [1,n] 区间,当我们找到区间最小值的下标 pos 之后,将区间分成 [1,pos-1] 和 [pos+1,n] 两个区间,此时我们有两种选择: 1. 让区间 [1,pos-1] 都等于 m[pos] 并递归的求区间 [pos+1,n] 可能的最大建楼数; 2. 让区间 [pos+1,n] 都等于 m[pos] 并递归的求区间 [1,pos-1] 可能的最大建楼数; 那我们可以分别算出这两种选择最终的最大可能建楼数,取两者较大的那个并更新答案即可。
C1:n <= 1000
上述分治步骤的复杂度是 O(n),此题可以通过 O(n) 找到每个区间的最小值的下标,那么总的复杂度就是 O(n^2)。
#include <bits/stdc++.h> #define LL long long using namespace std; const int N = 1e6 + 5; int a[N], ans[N]; int n; pair < int, int > query(int l, int r) { /// 求解区间 [l,r] 的最小值和最小值位置 int pos = l, mi = a[l]; for(int i = l; i <= r; i++) { if(a[i] < mi) { mi = a[i]; pos = i; } } return make_pair(mi, pos); } LL solve(int l, int r) { if(l == r) { /// 递归出口 ans[l] = a[l]; return 1LL * a[l]; } if(l > r) return 0LL; pair < int, int > tmp = query(l, r); /// 找到区间 [l, r] 的最小值和最小值位置,first是最小值,second是最小值的位置 int pos = tmp.second; /// 分治求解两个区间 LL ans1 = solve(l, pos - 1) + 1LL * (r - pos + 1) * a[pos]; LL ans2 = solve(pos + 1, r) + 1LL * (pos - l + 1) * a[pos]; /// 取最优解并保存答案 if(ans1 >= ans2) { for(int i = pos; i <= r; i++) ans[i] = tmp.first; return ans1; } else { for(int i = l; i <= pos; i++) ans[i] = tmp.first; return ans2; } } int main() { scanf("%d", &n); for(int i = 1; i <= n; i++) scanf("%d", &a[i]); solve(1, n); /// 分治求解 for(int i = 1; i <= n; i++) printf("%d ", ans[i]); return 0; }
C2:n <= 500000
此时不能再 O(n) 的去找每个区间的最小值的下标了,可以使用线段树来维护区间的最小值及其下标,这样只需 O(logn) 就能找到区间的最小值的下标,总的复杂度就是 O(nlogn)。
#include <bits/stdc++.h> #define LL long long #define INF INT_MAX using namespace std; const int N = 1e6 + 5; int a[N], ans[N]; int n; pair < int, int > t[N << 2]; void build(int rt, int l, int r) { if(l == r) { t[rt] = make_pair(a[l], l); return ; } int mid = (l + r) >> 1; build(rt << 1, l, mid); build(rt << 1 | 1, mid + 1, r); if(t[rt << 1].first <= t[rt << 1 | 1].first) t[rt] = t[rt << 1]; else t[rt] = t[rt << 1 | 1]; } pair < int, int > query(int rt, int l, int r, int L, int R) { if(L <= l && r <= R) return t[rt]; int mid = (l + r) >> 1; pair < int, int > ans = make_pair(INF, 0); if(L <= mid) { pair < int, int > tmp = query(rt << 1, l, mid, L, R); if(tmp.first < ans.first) ans = tmp; } if(R > mid) { pair < int, int > tmp = query(rt << 1 | 1, mid + 1, r, L, R); if(tmp.first < ans.first) ans = tmp; } return ans; } LL solve(int l, int r) { if(l == r) { /// 递归出口 ans[l] = a[l]; return 1LL * a[l]; } if(l > r) return 0LL; pair < int, int > tmp = query(1, 1, n, l, r);/// 找到区间 [l, r] 的最小值和最小值位置,first是最小值,second是最小值的位置 int pos = tmp.second; /// 分治求解两个区间 LL ans1 = solve(l, pos - 1) + 1LL * (r - pos + 1) * a[pos]; LL ans2 = solve(pos + 1, r) + 1LL * (pos - l + 1) * a[pos]; /// 取最优解并保存答案 if(ans1 >= ans2) { for(int i = pos; i <= r; i++) ans[i] = tmp.first; return ans1; } else { for(int i = l; i <= pos; i++) ans[i] = tmp.first; return ans2; } } int main() { scanf("%d", &n); for(int i = 1; i <= n; i++) scanf("%d", &a[i]); build(1, 1, n); /// 建线段树 solve(1, n); /// 分治递归求解 for(int i = 1; i <= n; i++) printf("%d ", ans[i]); return 0; }
方法二
根据题目的条件,我们可以想象最终的 ans 数组一定是类似于那种“山峰”的感觉。也就是说最终的 ans 数组存在一个最高点,假设其下标为 pos,那么数组 ans 在区间 [1,pos] 一定是非递减的(即 ans[1]<=...<=ans[pos]),在区间 [pos,n] 一定是非递增的(即ans[pos]<=...<=ans[n])。
理解了以上的特征,我们可以 O(n) 枚举这个最高点,并算出以这个点为最高点能建的最大可能楼层数。 现在我们已经确定了最高点,我们假设其下标为 pos,即 ans[pos] = m[pos],对于 pos-1,它一定不能高于 ans[pos],那么 ans[pos-1] 等于 min(ans[pos], m[pos-1]) 是最优的,对于 pos + 1 也是同理,ans[pos+1] 等于 min(ans[pos], m[pos+1]) 是最优的。 那我们就可以根据这种策略确定其他点的取值,算出以这个点为最高点能建的最大可能楼层数。 最后对所有的最大可能楼层数取一个最大值,并且维护是以哪个位置为最高点取到的这个最大值,根据这个位置去生成 ans 数组即可。
C1:n <= 1000
我们可以 O(n) 的枚举最高点,确定了最高点再 O(n) 的去取其他点的最优值并计算总楼层数,维护一个最大值,总的复杂度是 O(n^2)
#include <bits/stdc++.h> #define LL long long using namespace std; const int N = 1e6 + 5; int a[N],ans[N],tmp[N]; int main() { int n; scanf("%d", &n); for(int i = 1; i <= n; i++) scanf("%d", &a[i]); LL ma = 0; int ma_pos = 1; /// 定义一个最大值,和取得最大值的位置 pos for(int i = 1; i <= n; i++) { /// 枚举最高点 LL res = a[i]; tmp[i] = a[i]; for(int j = i-1; j >= 1; j--) { /// 贪心的求其他位置,并累加起来 tmp[j] = min(tmp[j+1], a[j]); res += tmp[j]; } for(int j = i+1; j <= n; j++) { tmp[j] = min(tmp[j-1],a[j]); res += tmp[j]; } if(res > ma) { /// 若比最大值大,更新答案 ma = res; ma_pos = i; } } ans[ma_pos] = a[ma_pos]; /// 根据取得最大值的位置,求出答案数组。 for(int i = ma_pos-1; i >= 1; i--) { ans[i] = min(ans[i+1], a[i]); } for(int i = ma_pos + 1; i <= n; i++) { ans[i] = min(ans[i-1], a[i]); } for(int i = 1; i <= n; i++) printf("%d ", ans[i]); return 0; }
C2:n <= 500000
这个时候仍然 O(n) 枚举最高点,但是不能再 O(n) 的去取其他点了。假设当前最高点的下标为 pos,我们要计算 ans 在区间 [1,pos] 能建的最大可能楼层数。确定了最高点及其下标 pos,那么 ans[pos]=m[pos],而对于 i 从 pos-1 到 1,若 m[i]>m[pos],则 ans[i]=m[pos];若m[i]<=m[pos],则 ans[i]=m[i],并且 i 从这里开始一直到 1 都不再受 m[pos] 影响,此时对于 j 从 i 到 1 这个区间来讲,最高点就降为了 m[i]。那我们可以用 pre[i] 来表示区间 [1,i] 以 m[i] 为最高点能建的最大可能楼层数,则 pre[pos]=pre[i]+(pos-i)*m[pos]
观察到对于每个 pre[i] 我们都需要找到数组 m 在区间 [1,i-1] 中最后一个小于等于 m[i] 的数的位置。 这一步骤可以使用单调栈来完成,那我们就可以利用单调栈,O(n) 的预处理出数组 pre。 而此时的 pre[i] 只是处理了区间 [1,i] 的问题,还有区间 [i,n] 没有处理,则需要引入 suc[i] 表示区间 [i,n] 以 m[i] 为最高点能建的最大可能楼层数。此时 suc[i] 需要找到数组 m 在区间 [i+1,n] 中第一个小于等于 m[i] 的数的位置 pos,这样就有 suc[i]=suc[pos]+(pos-i)*m[i],依然利用单调栈来预处理出数组 suc。 这样,以 m[i] 作为最高点能建的最大可能楼层数为 pre[i]+suc[i]-m[i]。 预处理数组的复杂度是 O(n),枚举最高点也是 O(n),那么总的复杂度就是 O(n)。
#include <bits/stdc++.h> #define LL long long #define mem(i, j) memset(i, j, sizeof(i)) #define rep(i, j, k) for(int i = j; i <= k; i++) #define dep(i, j, k) for(int i = k; i >= j; i--) #define pb push_back #define make make_pair #define INF INT_MAX #define inf LLONG_MAX #define PI acos(-1) using namespace std; const int N = 1e6 + 5; int a[N], pre[N], suc[N], s[N], tot, ans[N]; /// 这里的 pre[i] 是区间 [1,i-1] 中最后一个小于等于 a[i] 的位置,pre_sum[i] 才是它的总楼层,suc[i] 同理。 LL pre_sum[N], suc_sum[N]; int main() { int n; scanf("%d", &n); for(int i = 1; i <= n; i++) scanf("%d", &a[i]); /// 预处理出 pre[i], pre_sum[i] for(int i = 1; i <= n; i++) { while(tot && a[i] < a[s[tot]]) tot--; pre[i] = s[tot]; pre_sum[i] = pre_sum[s[tot]] + 1LL * (i - s[tot]) * a[i]; s[++tot] = i; } /// 预处理出 suc[i], suc_sum[i] tot = 0; s[0] = n + 1; for(int i = n; i >= 1; i--) { while(tot && a[i] < a[s[tot]]) tot--; suc[i] = s[tot]; suc_sum[i] = suc_sum[s[tot]] + 1LL * (s[tot] - i) * a[i]; s[++tot] = i; } LL ma = 0LL; int pos = -1; /// 定义一个最大值,和取得最大值的位置 for(int i = 1; i <= n; i++) { if(pre_sum[i] + suc_sum[i] - 1LL * a[i] > ma) { ///维护最大值 ma = pre_sum[i] + suc_sum[i] - 1LL * a[i]; pos = i; } } for(int i = pos; i; i = pre[i]) { /// 求出答案数组 rep(j, pre[i] + 1, i) ans[j] = a[i]; } for(int i = pos; i <= n ; i = suc[i]) { rep(j, i, suc[i] - 1) ans[j] = a[i]; } for(int i = 1; i <= n; i++) printf("%d ", ans[i]); return 0; }