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;
}
View Code

 

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;
}
View Code

 

方法二

根据题目的条件,我们可以想象最终的 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;
}
View Code

 

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;
}
View Code

 

posted on 2021-05-16 16:44  Willems  阅读(67)  评论(0编辑  收藏  举报

导航