CF1446D1, D2 Frequency Problem
题目来源:Codeforces, Codeforces Round #683 (Div. 1, by Meet IT),CF1446D1 Frequency Problem (Easy Version),CF1446D2 Frequency Problem (Hard Version)
题目大意
有一个长度为 (n) 的序列 (a_1,a_2,dots ,a_n)。
我们定义一个子段(连续子序列)是好的,当且仅当其中出现次数最多的元素不唯一。
请求出,最长的、好的子段的长度。
数据范围:(1leq nleq 2 imes10^5)。对于 D1,(1leq a_ileq min(n,100)),对于 D2,(1leq a_ileq n)。
本题题解
D1
如果原序列里,出现次数最多的数不唯一,则答案就是 (n)。否则,考虑那个唯一的、出现次数最多的元素,记为 (x)。
发现,最长的、好的子段(也就是答案),有个必要条件是:(x) 是其中出现次数最多的元素之一。
证明:考虑一个好的子段 ([l,r]),如果 (x) 不是其中出现次数最多的元素。那么,我们依次考虑 ([l-1,r]), ([l-2,r]), ... ..., ([1,r]), ([1,r+1]), ([1,r+2]), ... ..., ([1,n])。因为 (x) 的数量每次最多增加 (1) 个,且它是整个序列里出现次数最多的元素。那么总有某一步后,(x) 和序列里其他出现最多的元素一样多。
我们枚举,答案的子段里,和 (x) 出现次数一样多的元素是谁,记为 (y)。问题转化为,对一组数值 ((x,y)) 计算答案。
我们可以忽略数列里除 (x), (y) 外的其他数。问题进一步转化为:求一个最长的子段,满足 (x), (y) 的出现次数一样多。
把数列里的 (x) 看做 (1),(y) 看做 (-1),其他数看做 (0)。得到一个序列 (b)。那么,我们要求最长的、和为 (0) 的子段长度。做前缀和,令 ( ext{sum}_i=sum_{j=1}^{i}b_j)。问题相当于找两个位置 (0leq l < rleq n),满足 ( ext{sum}_r = ext{sum}_l)。对每个 ( ext{sum}) 值 (v)(([-n,n]),共 (2n+1) 个值)记录它最早的出现位置。然后枚举 (r),并更新答案即可。
有读者可能对“忽略数列里除 (x), (y) 外的其他数”这一做法心存疑虑。因为有可能,我们找到的序列里,存在一个其他数值 (z),出现次数多于 (x,y)。但是,基于以下两点:
- 存在这样 "(z)" 的子段,长度一定小于答案。证明:我们按和上面同样的方法,向两边扩展一下,直到 (x) 和 (z) 出现次数相等。得到的序列仍然满足:存在两个数 ((x,z)) 出现次数相等,且新的子段长度比原来长。所以只要存在这样的 "(z)",我们就扩展一下,最终一定能得到没有 "(z)" 的,也就是以 (x) 为出现次数最多的数的子段,且长度一定严格大于原来的子段。
- 答案的子段,一定会被考虑到。因为只要满足 (x), (y) 出现次数相等,就会被考虑到。
我们用这种方法,求出的就是答案。
时间复杂度 (O(n imes ext{maxa})=O(n imes 100))。可以通过 D1。
D2
考虑答案序列里,出现次数最多的数,的出现次数。如果出现次数 (>sqrt{n}),则满足条件的数值只有不超过 (sqrt{n}) 个,我们套用 D1 的做法,时间复杂度 (O(nsqrt{n}))。
如果出现最多的数,出现次数 (leq sqrt{n}),我们枚举这个出现次数,记为 (t)。然后做 two pointers。
具体来说,枚举区间的右端点 (r)。对每个 (r),记最小的,使得 ([l,r]) 内所有数出现次数都 (leq t) 的 (l) 为 (L_r)。容易发现,对于 (r=1,2,dots,n),(L_r) 单调不降。并且,在当前的 (t) 下,对一个 (r) 来说,如果 ([L_r,r]) 不是好的子段(出现次数为 (t) 的数值,不到 (2) 个),那么其他 ([l,r]) ((L_r<lleq r)) 也不可能是。因为出现次数为 (t) 的数值,只会更少。
所以,随着我们从小到大枚举 (r),只需要用一个变量来记录当前的 (L_r)。同时用数组维护一下,当前每个数的出现次数,以及每个出现次数的出现次数。就能确定,当前的 ([L_r,r]) 是否是好的区间。具体可以见代码。这个 two pointers 的过程是 (O(n)) 的。因为有 (sqrt{n}) 个 (t),所以这部分的时间复杂度也是 (O(nsqrt{n}))。
总时间复杂度 (O(nsqrt{n}))。
总结
本题首先需要一个结论。通过观察、分析题目性质,是可以想到的。
然后需要一个技巧:把一个普通序列,转化为只含 (0), (1), (-1) 的序列。这样就把复杂的、出现次数的关系,转化为了简单的数值关系。这个技巧在一些经典的二分中位数的题里也有用到(把 (> ext{mid}) 的视为 (1),(leq ext{mid}) 的视为 (-1))。
D2,想到根号分治以后,应该就不是太难了。
参考代码
D1:
// problem: CF1446D1
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 2e5;
const int INF = 1e9;
int n, mxa, a[MAXN + 5];
int cnt[MAXN + 5], mp[MAXN * 2 + 5];
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
ckmax(mxa, a[i]);
cnt[a[i]]++;
}
int mx = 0, semx = 0;
for (int i = 1; i <= mxa; ++i) {
if (cnt[i] > cnt[mx]) {
semx = mx;
mx = i;
} else if (cnt[i] > cnt[semx]) {
semx = i;
}
}
// cerr << mx << " " << semx << endl;
if (!semx) {
// 整个序列只有一种值
cout << 0 << endl;
return 0;
}
if (cnt[semx] == cnt[mx]) {
cout << n << endl;
return 0;
}
int ans = 0;
for (int v = 1; v <= mxa; ++v) {
if (v == mx)
continue;
if (!cnt[v])
continue;
for (int i = 0; i <= n * 2; ++i) {
mp[i] = INF;
}
mp[n] = 0;
int presum = 0;
for (int i = 1; i <= n; ++i) {
int x = 0;
if (a[i] == mx) x = 1;
else if (a[i] == v) x = -1;
presum += x;
ckmax(ans, i - mp[presum + n]);
ckmin(mp[presum + n], i);
}
}
cout << ans << endl;
return 0;
}
D2:
// problem: CF1446D2
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 2e5;
const int INF = 1e9;
const int BOUND = 448;
int n, mxa, a[MAXN + 5];
int cnt[MAXN + 5], mp[MAXN * 2 + 5];
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
ckmax(mxa, a[i]);
cnt[a[i]]++;
}
int mx = 0, semx = 0;
for (int i = 1; i <= mxa; ++i) {
if (cnt[i] > cnt[mx]) {
semx = mx;
mx = i;
} else if (cnt[i] > cnt[semx]) {
semx = i;
}
}
// cerr << mx << " " << semx << endl;
if (!semx) {
// 整个序列只有一种值
cout << 0 << endl;
return 0;
}
if (cnt[semx] == cnt[mx]) {
cout << n << endl;
return 0;
}
int ans = 0;
for (int v = 1; v <= mxa; ++v) {
if (v == mx)
continue;
if (cnt[v] <= BOUND)
continue;
for (int i = 0; i <= n * 2; ++i) {
mp[i] = INF;
}
mp[n] = 0;
int presum = 0;
for (int i = 1; i <= n; ++i) {
int x = 0;
if (a[i] == mx) x = 1;
else if (a[i] == v) x = -1;
presum += x;
ckmax(ans, i - mp[presum + n]);
ckmin(mp[presum + n], i);
}
}
for (int frq = 1; frq <= BOUND; ++frq) {
for (int i = 1; i <= mxa; ++i) {
cnt[i] = 0;
}
for (int i = 1; i <= n; ++i) {
mp[i] = 0;
}
int l = 1;
for (int r = 1; r <= n; ++r) {
mp[cnt[a[r]]]--;
cnt[a[r]]++;
mp[cnt[a[r]]]++;
while (cnt[a[r]] > frq) {
mp[cnt[a[l]]]--;
cnt[a[l]]--;
mp[cnt[a[l]]]++;
++l;
}
if (mp[frq] >= 2) {
ckmax(ans, r - l + 1);
}
}
}
cout << ans << endl;
return 0;
}