2024牛客多校第9场
9
B Break Sequence (B)
似乎不止一次遇到线段树优化dp了,但仍然没做出来()
\(dp[i]\) 表示到 \(i\) 位置为止、将序列分成若干段的情况总数,一个显而易见的 \(n^2\) 做法是从 \(1\) 到 \(i - 1\) 枚举 \(j\),若 \(j + 1\) 至 \(i\) 为合法区间,则有 \(dp[i] = \sum\limits dp[j]\). 假设当前数字 \(a[i]\) 是第 \(k\) 次出现,对于集合 \(s\) 中的某个元素 \(x \leq k\),其对应的不合法位置是 \(a[i]\) 第 \(k - x\) 次至 \(k - x + 1\) 次出现之前的整个区间,因此可用线段树标记所有不合法区间,每次查询即寻找未被标记的 \(\sum\limits dp[j]\);维护 \(sum\) 作为当前区间标记值最小的位置权值和,即有每次标记完成后 \(dp[i] = sum[1]\). 一次操作中最多修改 \(2m\) 次标记(撤销先前标记、加上新标记),整体复杂度 \(m\cdot n\log n\).
线段树代码:
void pushup(int i) {
cnt[i] = min(cnt[i * 2], cnt[i * 2 + 1]);
if(cnt[i * 2] == cnt[i * 2 + 1]) {
sum[i] = sum[i * 2] + sum[i * 2 + 1]; // 标记值相等时直接合并
if(sum[i] >= mo) sum[i] -= mo;
} else {
if(cnt[i * 2] < cnt[i * 2 + 1]) sum[i] = sum[i * 2]; // 取最小值计入sum[i]中
else sum[i] = sum[i * 2 + 1];
}
}
// 懒标记针对cnt,pushdown中并不需要更新sum
void pushdown(int i) {
if(lz[i]) {
cnt[i * 2] += lz[i];
cnt[i * 2 + 1] += lz[i];
lz[i * 2] += lz[i];
lz[i * 2 + 1] += lz[i];
lz[i] = 0;
}
}
// update单点修改sum,用于将循环中获得的dp值加入线段树中
void update(int i, int l, int r, int q, ll val) {
if(l == r) {
sum[i] = val;
return;
}
pushdown(i);
int mid = (l + r) / 2;
if(q <= mid) update(i * 2, l, mid, q, val);
else update(i * 2 + 1, mid + 1, r, q, val);
pushup(i);
}
void modify(int i, int l, int r, int ql, int qr, int d) {
if(ql <= l && qr >= r) {
cnt[i] += d;
lz[i] += d;
return;
}
pushdown(i);
int mid = (l + r) / 2;
if(ql <= mid) modify(i * 2, l, mid, ql, qr, d);
if(qr > mid) modify(i * 2 + 1, mid + 1, r, ql, qr, d);
pushup(i);
}
main函数中主要代码:
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
v[i].push_back(0); // vector保存每个数字出现的位置
}
for(int i = 1; i <= m; i++) {
scanf("%d", &s[i]);
}
update(1, 0, n, 0, 1); // 初值
for(int i = 1; i <= n; i++) {
v[a[i]].push_back(i);
int cur = v[a[i]].size() - 1;
for(int j = 1; j <= m; j++) {
if(cur >= s[j]) {
modify(1, 0, n, v[a[i]][cur - s[j]], v[a[i]][cur - s[j] + 1] - 1, 1); // 不合法标记
}
if(cur > s[j]) {
modify(1, 0, n, v[a[i]][cur - s[j] - 1], v[a[i]][cur - s[j]] - 1, -1); // 撤销先前的标记
}
}
ll x = sum[1];
if(i == n) printf("%lld", x);
update(1, 0, n, i, x); // 更新dp值
}
C Change Matrix (C)
非常巧妙的数学题,似乎默认了数学可以交给队友()导致很久没练过gcd和排列组合以外的数学题了,现在这么菜是有原因的。
对于内部元素不同的矩阵,计算其行/列倍乘后的总和比较困难,若矩阵中元素完全一致则相对简单。假设 \(n\cdot m\) 的矩阵初始值为 \(1\),第 \(i\) 行倍乘系数为 \(a_i\),第 \(j\) 列倍乘系数 \(b_j\),则有:\(sum = (a_1b_1 + a_1b_2 + ... + a_1b_m) + (a_2b_1 + a_2b_2 + ...+ a_2b_m) + ... + (a_nb_1 + a_nb_2 + ... + a_nb_m) = a_1\cdot\sum\limits_{j = 1} ^ m b_j + a_2\cdot\sum\limits_{j = 1}^m b_j + ... + a_n\cdot\sum\limits_{j = 1} ^ m b_j = (\sum\limits_{i = 1} ^ n a_i)(\sum\limits_{j = 1} ^ m b_j)\);若初始值为 \(x\),则 \(sum' = sum \cdot x\);可通过 \(a,b\) 数列动态维护。
矩阵内部元素为 \(gcd(i, j)\),其分布具有规律性,如下图所示:
(图源b站牛客竞赛,雨巨讲题视频)
将整个矩阵视为多个小矩阵的叠加和,其中每个小矩阵的内部初值一致。图中颜色相同的位置含有相同的因数 \(i\),可将其当作一个小矩阵,设原矩阵中某位置元素为 \(x\),由于 \(x = \sum\limits_{\substack{i=1\\i|x}}^x \varphi(i)\)(这个证明好麻烦,不想写了,手算也能发现是成立的),小矩阵初值即欧拉函数 \(\varphi(i)\). 维护 \(n\) 个小矩阵的 \(a,b\) 倍乘系数,每个矩阵的大小为 \(n / i\),对第 \(k\) 行的倍乘即对 \(k\) 的所有因数 \(i\) 改变 \(a_{k / i}\),可 \(O(1)\) 计算答案变化量;对列的倍乘同理。
筛法求欧拉函数:
vector <int> p;
bool no[N];
ll phi[N];
void ori(int n) {
phi[1] = 1;
for(int i = 2; i <= n; i++) {
if(!no[i]) {
p.push_back(i);
phi[i] = i - 1;
}
for(int j = 0; j < p.size(); j++) {
if(i * p[j] > n) break;
no[i * p[j]] = 1;
if(i % p[j] == 0) {
phi[i * p[j]] = phi[i] * p[j];
break;
}
phi[i * p[j]] = phi[i] * phi[p[j]];
}
}
}
主要代码:
ll ans = 0;
for(int i = 1; i <= n; i++) {
r[i].resize(n / i + 1, 1); // r,c即题解中a,b数列
c[i].resize(n / i + 1, 1);
rsum[i] = csum[i] = n / i; // 维护每个矩阵r,c之和
ans += rsum[i] * csum[i] % mo * phi[i] % mo;
ans %= mo;
for(int j = 1; i * j <= n; j++) {
fac[i * j].push_back(i); // 预处理出1-n的所有因数
}
}
while(q--) {
char ch = getchar();
while(ch != 'R' && ch != 'C') ch = getchar();
int x, t;
scanf("%d%d", &x, &t);
for(int i = 0; i < fac[x].size(); i++) {
int j = fac[x][i];
if(ch == 'R') {
rsum[j] += (t - 1) * r[j][x / j] % mo;
rsum[j] %= mo;
ans += (t - 1) * r[j][x / j] % mo * csum[j] % mo * phi[j] % mo;
ans %= mo;
r[j][x / j] *= t;
r[j][x / j] %= mo;
} else {
csum[j] += (t - 1) * c[j][x / j] % mo;
csum[j] %= mo;
ans += (t - 1) * c[j][x / j] % mo * rsum[j] % mo * phi[j] % mo;
ans %= mo;
c[j][x / j] *= t;
c[j][x / j] %= mo;
}
}
printf("%lld\n", ans);
}
即使队友很擅长的领域也要自己学明白啊QwQ 像之前认真练数据结构的时候,心里想的是万一哪天被他俩踢出来我得自力更生(?)