• 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
    一步一步,永不停息
  • 相关阅读:
    第二次团队作业
    第一次团队作业
    软件工程结对编程第二次作业
    第四次软件工程作业
    Hadoop综合大作业
    hive基本操作与应用
    熟悉HBase基本操作
    爬虫大作业(爬取广州番禺职业技术学院新闻发布方)
    熟悉常用的HDFS操作
    数据结构化与保存
  • 原文地址:https://www.cnblogs.com/Willems/p/14774104.html
Copyright © 2020-2023  润新知