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
:
然后再取最大值
对于环,我们考虑将数组复制一倍,按照普通的最大子段和的做法来做
但这又有一个问题,如果求出的最大子段和的区间长度是超过了 \(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]
的最大值即可
环状最大双子段和
类似环状最大子段和,分类讨论:
-
如果子段都没有越过
n
去到1
,那么就是普通的最大双子段和
-
如果越过了
n
去到1
,那么就求出最小双子段和
,用所有数的和减去它即可
最后两者求最大值即可