题目大意
给定由 (n(1leq nleq 2cdot 10^5)) 个正整数组成的数列({a_n}),你需要把这个数列分成若干个连续段,使得每个连续段内任意两个不同位置上数字的乘积不为完全平方数。初始时你可以把不多于 (k (0leq kleq 20)) 个位置上的数修改成任意正整数。请最小化分出的连续段的数目。
题解
容易发现,若两个数的乘积为完全平方数,则两个数分别除尽各自的平方因子后相同。所以我们可以先除尽数列中每个数的平方因子,问题就转化为要使得每个连续段中的数是互不相同的。
设 (l[i][x]) 表示以第 (i) 个位置结尾的连续段,之前修改不超过 (x) 个位置,该段所能延伸到的最远的左端点的位置。
可以发现,若我们固定住 (x),不断递增 (i),(l[i][x]) 是单调右移的,我们可以维护一个单调右移的指针,并且开一个桶记录该指针到 (i) 之间每个数出现的次数,若当前存在两个数出现了两次及以上,就要右移指针,直到每个数在该段内只出现一次。于是我们可以以 (O(nk)) 的时间复杂度求得所有 (l[i][x])。
设 (dp[i][j]) 表示前 (i) 个位置中修改了不超过 (j) 个位置,所划分出的最少的连续段数。
那么有 (dp[i][j]=min(dp[i][j],dp[l[i][x]-1][j-x]+1))
时间复杂度 (O(nk^2))。
ps: "维护每个位置最远能延伸到的地方"这种思想挺重要的,被这坑了很多次了。
Code
#include <bits/stdc++.h>
using namespace std;
#define RG register int
#define LL long long
template<typename elemType>
inline void Read(elemType& T) {
elemType X = 0, w = 0; char ch = 0;
while (!isdigit(ch)) { w |= ch == '-';ch = getchar(); }
while (isdigit(ch)) X = (X << 3) + (X << 1) + (ch ^ 48), ch = getchar();
T = (w ? -X : X);
}
int dp[200001][21], l[200001][21], a[200001], b[10000010];
int cnt[10000010];
int T, N, K;
int div(int x) {
int res = 1;
for (LL i = 2; i * i <= x; ++i) {
int cnt = 0;
while (x % i == 0) { x /= i; ++cnt; }
if (cnt & 1) res *= i;
}
if (x != 1) res *= x;
return res;
}
void Solve() {
for (int k = 0;k <= K;++k) {
int num = 0, p = 1;
for (int i = 1;i <= N;++i) {
++cnt[a[i]];
if (cnt[a[i]] > 1) ++num;
while (num > k) {
if (cnt[a[p]] > 1) --num;
--cnt[a[p]];++p;
}
l[i][k] = p;
}
while (p <= N) { cnt[a[p]] = 0; ++p; }
}
for (int i = 1;i <= N;++i)
for (int k = 0;k <= K;++k)
for (int x = 0;x <= k;++x)
dp[i][k] = min(dp[i][k], dp[l[i][x] - 1][k - x] + 1);
int ans = 1 << 30;
for (int k = 0;k <= K;++k)
ans = min(ans, dp[N][k]);
printf("%d
", ans);
}
int main() {
Read(T);
while (T--) {
Read(N);Read(K);
for (int i = 1;i <= N;++i) {
Read(a[i]);
if (!b[a[i]]) b[a[i]] = div(a[i]);
a[i] = b[a[i]];
for (int j = 0;j <= K;++j)
dp[i][j] = l[i][j] = 0x3f3f3f3f;
}
Solve();
}
return 0;
}