P6170 [USACO16FEB]Circular Barn G

传送门


思路

(虽然是贺的,但很有收获,写篇题解总结一下)

我们应该能想到一个贪心的策略:如果一头牛移动的路径上(不包括起点)经过了其他奶牛,那么这个一定不优

比如说:\(x,y\) 上各有一头奶牛,现在要让 \(z\) 上有一头奶牛(\(x<y<z\)),如果移动 \(x\)\(z\),显然不如先将 \(y\) 移到 \(z\),再将 \(x\) 移到 \(z\) 优,因为 \(a^2+b^2\le (a+b)^2\) (可以将 \(x\rightarrow y\) 的距离看做 \(a\)\(y\rightarrow z\) 的距离看做 \(b\)

因此我们要找到一个起点,使得我们从这里开始移动的话,每一步都是合法的(这里的“合法的”指的是满足上面的贪心策略)

这样的起点 \(s\) 应该要满足:\(\forall t\in[1, n]\),有 \(\sum_{i=r_s}^{r_t} c[i] > r_t-r_s+1\)(这里的 \(r_x\) 表示以环上某一点为起点,顺时针进行编号)

但我们“很难”知道哪些起点是合法的,那么我们就可以尝试一一枚举起点,这样做就是 \(O(n^2)\) 的,可以过银组的数据

具体方法就是:移到某个点时,留下一个点在当前位置,其他的点都移动到下一个点


对于这道题,我们一定要想办法优化掉一个 \(n\)

想到我们上面起点要满足的条件,那我们是不是找一个比较密集的子段的起点,就可以满足条件呢?

因为最后一定要留一个奶牛在当前位置,我们考虑将每个位置的 \(c[i]\) 都减 \(1\),得到 \(b[i]\)

实际上,求 \(b[i]\) 的环状最大子段和即可

这里顺便给出一个环状最大子段和的做法:

首先我们都知道普通的最大子段和就是一个这样的 dp

\[dp[i]=max\{dp[i-1]+a[i], a[i] \} \]

然后再取最大值

对于环,我们考虑将数组复制一倍,按照普通的最大子段和的做法来做

但这又有一个问题,如果求出的最大子段和的区间长度是超过了 \(n\) 的,这是非法的

对于这种情况,其实就是环上所有数的和,再减去一个最小子段和

这时的最小子段和一定不会出现跟上面一样的问题

求出最大子段和,那么我们将它的起点作为我们移动的起点,这样一定合法


#include<iostream>
#include<fstream>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<cstring>
#include<queue>
#include<map>
#include<set>
#include<bitset>
#define LL long long
inline int reads()
{
    int sign = 1, re = 0; char c = getchar();
    while(c < '0' || c > '9'){if(c == '-') sign = -1; c = getchar();}
    while('0' <= c && c <= '9'){re = re * 10 + (c - '0'); c = getchar();}
    return sign * re;
}
int n, a[100005], b[200005];
int dp[200005], st[200005]; bool ov;
int Mx, ans; LL sum;
std::deque<int> q[100005];
signed main()
{
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    n = reads();
    for(int i = 1; i <= n; i++) a[i] = reads(), b[i] = a[i] - 1;
    for(int i = 1; i <= n; i++) b[i + n] = b[i];
    for(int i = 1; i <= (n << 1); i++)
    {
        if(dp[i - 1] + b[i] > b[i]) 
            dp[i] = dp[i - 1] + b[i], st[i] = st[i - 1];
        else dp[i] = b[i], st[i] = i;
        if(i - st[i] >= n)
        {
            ov = true;
            break;
        }
    }
    if(ov)
    {
        for(int i = 1; i <= n; i++) Mx += b[i];
        for(int i = 1; i <= (n << 1); i++)
        {
            dp[i] = b[i], st[i] = i;
            if(dp[i - 1] + b[i] < dp[i])
                dp[i] = dp[i - 1] + b[i], st[i] = st[i - 1];
        }
        int Mn = 1e5 + 1;
        for(int i = 1; i <= (n << 1); i++)
            if(Mn > dp[i])
                Mn = dp[i], ans = i % n + 1;
    }
    else for(int i = 1; i <= (n << 1); i++)
            if(Mx < dp[i]) Mx = dp[i], ans = (st[i] - 1) % n + 1;
    for(int st = 1, i = ans; st <= n; st++, i = i % n + 1)
        for(int j = 1; j <= a[i]; j++)
            q[st].push_back(st);
    for(int i = 1; i <= n; i++)
        while(q[i].size() > 1)
        {
            int now = q[i].front(); q[i].pop_front();
            sum -= 1ll * (i - now) * (i - now);
            q[i + 1].push_back(now);
            sum += 1ll * (i + 1 - now) * (i + 1 - now);
        }
    printf("%lld", sum);
    return 0;
}

顺带讲讲最大双子段和

最大双子段和

我们可以从前往后做一次 最大子段和,再从后往前做一次;接着 Mx_front[i] 表示 \(i\) 前面的数最大的子段和,Mx_back[i] 表示 \(i\) 后面的数最大的子段和

然后枚举分界点 \(k\),取 Mx_front[k - 1] + Mx_back[i + 1] 的最大值即可

环状最大双子段和

类似环状最大子段和,分类讨论:

  1. 如果子段都没有越过 n 去到 1 ,那么就是普通的 最大双子段和

  2. 如果越过了 n 去到 1,那么就求出 最小双子段和,用所有数的和减去它即可

最后两者求最大值即可

posted @ 2022-05-25 16:31  zuytong  阅读(18)  评论(0编辑  收藏  举报