神秘题
Trick
-
排列置换题,考虑转化乘环上移动问题。(精灵之环)
-
关于异或题目,在二进制上考虑。(New Divide)
-
在 \(a_1,a_2,\dots,a_n\) 中选出一个数使得和 \(x\) 按位或/与最大,考虑用高维前缀和维护超集。(New Divide)
-
对于往后跳 \(k\in[1,n]\) 步,可以考虑调和级数、
bitset、根号分治。([CCPC 2023 北京市赛] 替换) -
异或有关的题目,考虑在二进制上找性质。(绝望)
-
\(O(2^nX)\) 的二进制题目过不去,考虑折半优化成 \(O(2^{\frac{n}{2}}X)\)。(高维前缀和)
-
优化 dp,可以考虑 dp 的本质(比如放在网格图上考虑之类的)。(双扩展序列)
-
与在序列上跳 \(k\) 步有关的东西,可以考虑倍增。(跳越)
-
某些贪心题,可以优先考虑满足更紧的限制。(CF1251E2 Voting (Hard Version))
-
若每次查寻的位置固定,那么可以值维护会被用到的位置,减少复杂度。(#P1356. 食物)
题目
精灵之环
中文题面:

假设知道排列 \(p\)。
那么把这个排列 \(p\) 的环连出来,环上点的编号是排列的下标,点的值是编号对应的值。
就比如排列 4 1 2 3 的环为:
val: 4 1 2 3 4
id : 1->2->3->4->1...
可以发现把这些环上的值移动一次会使得环上点的编号和值相等,此时就对应排列 \(1,2,3,4,\dots,n\),也就是 \(p_0\)。
然后考虑把 \(p_k\) 的环给连出来,现在对于 \(p_k\) 的一个长度为 \(len\) 的环,如果有 \(\gcd(len,k)=1\),那么就可以把这个环上的值移动 \(k\) 次变成编号与值对应相等的环。
如果不满足 \(\gcd(len,k)=1\),那么可以考虑把若干个长度为 \(len\) 的环拼起来,使得 \(\gcd(len\times t,k)=t\)(\(t\) 为环的个数),这样也可以使得把这个环上的值移动 \(k\) 次变成编号与值对应相等的环。
所以对于每种长度的环,找到一个最小的 \(t\) 使得 \(\gcd(len\times t,k)=t\),然后把这些环每 \(t\) 个拼一起即可,因为题目满足有解,所以一定能够找到这个 \(t\)。
知道了所有环,就可以求出 \(p\) 了。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
}
using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n, k;
int q[N], p[N];
vector< int> G[N];
vector< int> cle[N], ans[N];
vector< vector< int> > vec[N], V;
int vis[N], mp[N], cnt;
void dfs( int u, int id) {
if (vis[u]) return ;
vis[u] = 1, cle[id].push_back(u);
for ( auto v : G[u]) dfs(v, id);
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> k;
for ( int i = 1; i <= n; i ++)
cin >> p[i];
for ( int i = 1; i <= n; i ++) G[p[i]].push_back(p[p[i]]);
for ( int i = 1; i <= n; i ++)
if (! vis[i]) dfs(i, ++ cnt);
for ( int i = 1; i <= cnt; i ++) {
int len = (int)cle[i].size();
vec[len].push_back(cle[i]);
mp[len] = 1;
}
int tot = 0;
for ( int len = 1; len <= n; len ++) {
if (! mp[len]) continue ;
for ( auto v : vec[len]) {
V.push_back(v);
int siz = (int)V.size() * len;
if (__gcd(k, siz) == (int)V.size()) { // 此时 V.size() 就是 t
++ tot;
ans[tot].resize(siz);
for ( int i = 0; i < siz; i ++)
vis[i] = 0;
// 构造环
for ( auto u : V) {
int id = 0;
for ( int i = 0; i < siz; i ++)
if (! vis[i]) {
id = i;
break ;
}
for ( auto c : u)
ans[tot][id] = c, vis[id] = 1,
id = (id + k) % siz;
}
cnt = 0, V.clear();
}
}
}
// 最后在环上面移动一次就是 p(这里默认了环上的点编号与值相等,也就是 p_0 对应的环)
for ( int i = 1; i <= tot; i ++) {
int len = (int)ans[i].size();
for ( int j = 0; j < len - 1; j ++)
q[ans[i][j]] = ans[i][j + 1];
q[ans[i][len - 1]] = ans[i][0];
}
for ( int i = 1; i <= n; i ++) cout << q[i] << ' ';
cout << '\n';
return 0;
}
New Divide
中文题面:

设 \(pre_i\) 表示 \([1,i]\) 的异或和。
那么转化一下就是对每个前缀 \([1,i]\) 找一个 \(k\),使得 \(pre_k+(pre_i \oplus pre_k)\) 的值最大。
直接求是困难的,考虑放在二进制下观察。
pre_i: 1 1 0 0 ...
pre_k: 1 0 1 0 ...
sum : 1 1 2 0 ...
可以发现,如果 \(pre_i\) 二进制下某一位为 \(1\),那么不论 \(pre_k\) 这一位是什么,贡献都是 \(1\);如果 \(pre_i\) 某一位为 \(0\),那么 \(pre_k\) 这一位为 \(1\),贡献为 \(2\),否则贡献为 \(0\)。
所以只用最大化 \(pre_i\) 为 \(0\),\(pre_k\) 为 \(1\) 的位数。
还是不好求,但是发现如果把 \(pre_i\) 按位取反,就可以转化成 \(pre_i\) 与 \(pre_k\) 的按位与最大。(其实也可以不取反,不取反就是求按位或最大,这两个是等价的。)
现在问题变成了,给定一个 \(pre_i\) 记它取反后为 \(val\),要求在 \(pre_1,pre_2,\dots,pre_{i}\) 中选出一个数 \(x\),使得 \(x\vee val\) 最大。
这看上去可以用 Trie 求,但其实不行,因为当 \(val\) 的某一位为 \(0\) 时,走 \(0\) 或 \(1\) 的出边是不确定的。
还是考虑从高到低枚举 \(val\) 的每一位去贪,如果这一位为 \(1\),那么肯定是想在 \(pre_1,pre_2,\dots,pre_{i}\) 选一个这位同样为 \(1\) 的出来,这启发可以维护一个变量 \(now\),表示当前 \(x\) 的值是多少。
具体来说,如果 \(val\) 第 \(i\) 位为 \(1\),那么就看 \(pre_1,pre_2,\dots,pre_{i}\) 中是否有 \(now+2^i\) 的超集,若有就可以让 \(x\) 的第 \(i\) 位取 \(1\)。
这里就可以用高维前缀和了,\(mi_S\) 表示 \(S\) 的超集中编号最小的一个,初始 \(mi_{pre_i}=\min i\)。
只要满足 \(mi_{now+2^i}\le\) 当前前缀的编号,就可以让 \(x\) 第 \(i\) 位取 \(1\)。
最后算出答案即可。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = (1 << 20), M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n;
int a[N];
int mi[N];
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
memset(mi, 127, sizeof mi);
mi[0] = 0;
cin >> n;
for ( int i = 1; i <= n; i ++) cin >> a[i], a[i] ^= a[i - 1], mi[a[i]] = min(mi[a[i]], i);
for ( int i = 0; i < 20; i ++)
for ( int S = 0; S < N; S ++)
if (! (S & (1 << i))) mi[S] = min(mi[S], mi[S | (1 << i)]);
for ( int i = 1; i <= n; i ++) {
int val = (N - 1) ^ a[i], now = 0;
for ( int j = 19; j >= 0; j --)
if ((val & (1 << j)) && mi[now | (1 << j)] <= i) now |= (1 << j);
int res = ((val & a[mi[now]]) << 1) + a[i];
cout << res << ' ';
}
return 0;
}
[CCPC 2023 北京市赛] 替换
这个操作,看起来可以用 bitset 优化。
那么用三个 bitset 分别维护:初始 \(1\) 的位置、初始 \(?\) 的位置、最终是 \(1\) 的位置。
这样做,复杂度 \(O(\frac{n^2\ln n}{\omega})\)。
可以发现,在 \(k\) 很小的时候,用 bitset 做是很亏的,还不如直接暴力。
所以考虑根号分治,在 \(k\le \sqrt n\) 的时候,可以直接跑暴力,在 \(\gt \sqrt n\) 时,直接用 bitset。
复杂度是 \(O(n\sqrt n+\frac{n^2\ln{\sqrt n}}{\omega})\),吸个氧直接过。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n;
char s[N];
bitset< N> ans, sum, cnt;
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
cin >> (s + 1);
int blk = sqrt(n);
for ( int i = 1; i <= n; i ++)
if (s[i] == '1') sum[i] = 1;
else if (s[i] == '?') cnt[i] = 1;
for ( int k = 1; k <= n; k ++) {
ans = sum;
if (k <= blk) {
for ( int i = k + 1; i <= n; i ++)
if (s[i] == '?' && ans[i - k]) ans[i] = 1;
} else {
for ( int i = k; i <= n; i += k)
ans |= ((ans << k) & cnt);
}
cout << ans.count() << '\n';
}
return 0;
}
绝望
记 \(high(x)\) 表示 \(x\) 二进制上最高的 \(1\) 所在位。
首先,要让 \(x\oplus y\gt x\),那么要满足 \(x\) 的第 \(high(y)\) 位是 \(0\)。
所以可以把那些不在任何一个 \(high(a_i)\) 的位给扔掉,不考虑它们。
接着分析,又可以发现在最优情况下要使得 \(x\oplus y\gt x\),\(y\) 满足除了 \(high(y)\),其它位上的 \(1\),\(x\) 这一位也必须位 \(1\)。
考虑证明,如果 \(x\) 有一位是 \(0\),那么可以找到一个 \(z\) 满足 \(high(z)\) 在这一位。(一定可以找到,因为已经把没用的位给去掉了)
然后就可以这样操作:\(x\to (x\oplus z)\to (x\oplus z \oplus y)\to (x\oplus z\ oplus y\oplus z)\),最后也就等价于 \(x\oplus y\),但是多做了几次操作。
证毕。
得到这个后,就可以转化问题了。
每次操作一个 \(a_i\),就相当于把此时的数 \(now\) 的第 \(high(a_i)\) 位的 \(0\) 变成 \(1\),前面的一些 \(1\) 变成 \(0\)。
考虑 dp,记 \(f_k\) 表示最多操作多少次使得第 \(k\) 位的 \(0\) 变成 \(1\)。
转移就是:
复杂度 \(O(n\log^2 V)\)。
代码
#include <bits/stdc++.h>
#define int long long
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n;
int a[N], hi[N], f[N];
int vis[N];
void solve() {
cin >> n;
for ( int k = 1; k <= 62; k ++) vis[k] = 0, f[k] = 0;
for ( int i = 1; i <= n; i ++) {
cin >> a[i], hi[i] = 0;
int x = a[i];
while (x) x >>= 1, hi[i] ++;
vis[hi[i]] = 1;
}
int ans = 0;
for ( int k = 1; k <= 62; k ++) {
if (! vis[k]) continue ;
for ( int i = 1; i <= n; i ++) {
if (hi[i] != k) continue ;
int res = 0;
for ( int j = hi[i] - 1; j >= 1; j --)
if ((a[i] >> (j - 1)) & 1) res += f[j];
f[k] = max(f[k], 1 + res);
}
ans += f[k];
}
cout << ans << '\n';
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while (T --) solve();
return 0;
}
高维前缀和
题目纯诈骗。
直接暴力做复杂度 \(O(2^nk)\),感觉优化成 \(O(2^{\frac{n}{2}}k)\) 就可以过。
那么考虑如何折半,首先对于两个数 \(x\)、\(y\),那么 \(x\) 的二进制前半段异或上 \(y\) 的二进制前半段的 \(\rm popcount\) 再加上 \(x\) 的二进制后半段异或上 \(y\) 的二进制后半段的 \(\rm popcount\) 肯定是等于 \(x\) 异或 \(y\) 的 \(\rm popcount\) 的。
所以枚举每一个操作,然后枚举 \(0\sim 2^{\frac{n}{2}}-1\) 的所有数 \(x\),。
记 \(f_{x,c,t}\) 表示满足当前二进制前半段是 \(x\),\(C\) 的后半段是 \(c\),\(D-{\rm popcount}(x\oplus C 的前半段)=t\) 的操作的 \(X\) 的乘积。
这就相当于开了一个桶,在处理答案时可以直接从这个桶得到信息。
对一个数 \(x\) 计算答案时,枚举所有 \(C\) 的后半段,然后把对应的 \(f\) 乘起来即可。
复杂度就是 \(O(2^{\frac{n}{2}}k)\)。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("popcount.in", "r", stdin);
freopen("popcount.out", "w", stdout);
}
using namespace std;
const int N = 5e5 + 10, M = 2e5 + 10, inf = 1e9;
int n, mod, k;
int f[1 << 9][1 << 9][20];
signed main() {
Freopen();
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> mod >> k;
int hn = n / 2;
int lim = ((1 << hn) - 1);
for ( int S1 = 0; S1 <= lim; S1 ++)
for ( int S2 = 0; S2 <= lim; S2 ++)
for ( int i = 0; i < 20; i ++)
f[S1][S2][i] = 1;
while (k --) {
int c, d, x;
cin >> c >> d >> x;
for ( int S = 0; S <= lim; S ++) {
int cnt = __builtin_popcount((c & lim) ^ S);
int T = (c >> hn);
if (cnt <= d && d - cnt <= n - hn) f[S][T][d - cnt] = 1ll * f[S][T][d - cnt] * x % mod;
}
}
for ( int S = 0; S < (1 << n); S ++) {
int res = 1;
for ( int s = 0; s <= lim; s ++)
res = 1ll * res * f[S & lim][s][__builtin_popcount(s ^ (S >> hn))] % mod;
cout << res << ' ';
}
return 0;
}
[NOIP2023] 双序列拓展
神题!
题目可以转化成能否使得所有 \(f_i\gt g_i\) 或者 \(f_i\lt g_i\),只考虑处理 \(f_i\lt g_i\) 的情况,\(f_i\gt g_i\) 的情况交换 \(X\)、\(Y\) 即可。
首先有一个很简单的 \(O(qnm)\) 的 dp,记 \(dp_{i,j}\) 表示 \(X\) 的前 \(i\) 个数和 \(Y\) 的前 \(j\) 个数是否可以匹配,转移有:
考虑这个 dp 的本质,实际上是有一个 \(n\times m\) 的网格图,\(A_{i,j}=[X_i\lt Y_j]\)。
每次可以走右方的一个 \(1\)、下方的一个 \(1\)、右下方的一个 \(1\),问是否能够走到 \(n,m\)。
考虑特殊性质,首先如果满足 \(X_{min}\ge Y_{min}\) 或者 \(X_{max}\ge Y_{max}\),那么是一定无法满足的,在网格图上说也就是有一行或一列全是 \(0\)。
排除这种情况后,特殊情况就等价于第 \(n\) 行、第 \(m\) 列都是 \(1\),因为 \(Y\) 中所有数都比 \(X_{min}\) 大,\(X\) 中所有数都比 \(Y_{max}\) 小。
那么只用考虑是否能走到第 \(n-1\) 行或者第 \(m-1\) 列,不难发现这把问题的规模缩小了!
这启发可以继续递归下去,具体就是在剩下的 \(n-1\) 个数找到 \(X\) 的最小、最大,剩下 \(m-1\) 个数中找到 \(Y\) 的最小、最大,然后判断,继续递归即可。
特殊性质对正解的启发是很大的。
对于全部数据,可以考虑找出 \(X_{min}\) 的位置与 \(Y_{max}\) 的位置,它们所占据的两条垂直直线把网格图划分成了四个区域,需要判断的就是能否从左上角走到这两条直线、再从这两条直线走到右下角。
照搬特殊性质的做法,只需要对左上区域递归、对右下区域递归即可。
复杂度 \(O(q(n+m))\)。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 5e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int c, n, m, q;
int a[N], b[N], pa[N], pb[N];
int prex[2][N], prem[2][N], sufx[2][N], sufm[2][N];
void get() {
int mi = inf, mx = -inf, idi = 0, idx = 0;
for ( int i = 1; i <= n; i ++) {
if (mi > a[i]) mi = a[i], idi = i;
if (mx < a[i]) mx = a[i], idx = i;
prem[0][i] = idi, prex[0][i] = idx;
}
mi = inf, mx = -inf, idi = 0, idx = 0;
for ( int i = 1; i <= m; i ++) {
if (mi > b[i]) mi = b[i], idi = i;
if (mx < b[i]) mx = b[i], idx = i;
prem[1][i] = idi, prex[1][i] = idx;
}
mi = inf, mx = -inf, idi = 0, idx = 0;
for ( int i = n; i; i --) {
if (mi > a[i]) mi = a[i], idi = i;
if (mx < a[i]) mx = a[i], idx = i;
sufm[0][i] = idi, sufx[0][i] = idx;
}
mi = inf, mx = -inf, idi = 0, idx = 0;
for ( int i = m; i; i --) {
if (mi > b[i]) mi = b[i], idi = i;
if (mx < b[i]) mx = b[i], idx = i;
sufm[1][i] = idi, sufx[1][i] = idx;
}
}
int chk1( int x, int y) {
if (x == 1 || y == 1) return 1;
if (a[prem[0][x - 1]] < b[prem[1][y - 1]]) return chk1(prem[0][x - 1], y);
if (a[prex[0][x - 1]] < b[prex[1][y - 1]]) return chk1(x, prex[1][y - 1]);
return 0;
}
int chk2( int x, int y) {
if (x == n || y == m) return 1;
if (a[sufm[0][x + 1]] < b[sufm[1][y + 1]]) return chk2(sufm[0][x + 1], y);
if (a[sufx[0][x + 1]] < b[sufx[1][y + 1]]) return chk2(x, sufx[1][y + 1]);
return 0;
}
int solve() {
int F1 = 1, F2 = 1;
get();
if (a[1] >= b[1] || a[n] >= b[m]) F1 = 0;
if (a[prem[0][n]] >= b[prem[1][m]]) F1 = 0;
if (a[prex[0][n]] >= b[prex[1][m]]) F1 = 0;
F1 &= (chk1(prem[0][n], prex[1][m]) && chk2(prem[0][n], prex[1][m]));
swap(a, b), swap(n, m);
get();
if (a[1] >= b[1] || a[n] >= b[m]) F2 = 0;
if (a[prem[0][n]] >= b[prem[1][m]]) F2 = 0;
if (a[prex[0][n]] >= b[prex[1][m]]) F2 = 0;
F2 &= (chk1(prem[0][n], prex[1][m]) && chk2(prem[0][n], prex[1][m]));
swap(a, b), swap(n, m);
return (F1 | F2);
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> c >> n >> m >> q;
for ( int i = 1; i <= n; i ++) cin >> a[i], pa[i] = a[i];
for ( int i = 1; i <= m; i ++) cin >> b[i], pb[i] = b[i];
cout << solve();
for ( int o = 1; o <= q; o ++) {
int kx, ky;
cin >> kx >> ky;
while (kx --) {
int x, v; cin >> x >> v;
a[x] = v;
}
while (ky --) {
int x, v; cin >> x >> v;
b[x] = v;
}
cout << solve();
for ( int i = 1; i <= n; i ++) a[i] = pa[i];
for ( int i = 1; i <= m; i ++) b[i] = pb[i];
}
return 0;
}
跳越
给一个长度为 \(n\) 的 01 串 \(S\),给定每次可以跳的距离 \(k\)(可以从 \(i\) 跳到 \(\max(1,i-k)\) 或 \(\min (n,i+k)\))。
有 \(q\) 次询问,每次问从 \(a\) 跳到 \(b\) 踩到 \(0\) 的最小值,以及在满足踩到 \(0\) 最少的情况下,跳跃的最少次数。保证 \(S_a=S_b=1\)。
\(n,q\le 5\times 10^5\)。
首先最优的情况下一定不会往会跳,也就是说只会往一个方向跳,那么从 \(a\) 跳到 \(b\) 就等价于从 \(b\) 跳到 \(a\)。
所以只考虑 \(a\lt b\) 的情况。
首先有一个容易想到的贪心,即每次跳到能够跳到的最远的那个 \(1\),如果后面有大于 \(k\) 个 \(0\),就直接跳 \(k\) 步即可。
这个贪心对于第一个问题是正确的,但第二个问题就不对了。
这是很容易被 Hack 的,就比如 \(k=2\) 时对于 11001111,从 \(1\) 跳到 \(7\) 需要踩 \(1\) 次 \(0\),最少跳 \(3\) 次,但用上述贪心就会发现求出来的跳越次数是 \(4\) 次。
这是为什么?对于一个长度大于 \(k\) 的极长 \(0\) 段,如果从 \(i\) 跳到离它最近的一个 \(1\),再跳出极长 \(0\) 段所踩的 \(0\) 的个数等于从 \(i\) 直接跳进极长 \(0\) 段,再跳出去所踩的 \(0\) 的个数相等,肯定是选择后者更优,因为踩的 \(0\) 个数相同,且步数更少。
现在就可以得道一个倍增做法了,记 \(f_{i,j}\) 表示从 \(i\) 开始跳 \(2^j\) 步时最优决策下的位置,\(g_{i,j}\) 表示从 \(i\) 开始跳 \(2^j\) 步时最优决策下的位置所踩到的 \(0\) 的个数。
只需要保证 \(f_{i,0}\) 的处理正确即可。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
int Test, n, q, k, t;
char s[N];
int to[N], f[N][20], g[N][20];
vector< int> vec;
signed main() {
ios :: sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> Test >> n >> q >> k >> t;
cin >> (s + 1);
for ( int i = 1; i <= n; i ++) if (s[i] == '1') vec.push_back(i);
for ( int i = 1; i <= n; i ++) {
int j = upper_bound(vec.begin(), vec.end(), i + k) - vec.begin() - 1;
if (vec[j] <= i) to[i] = min(n, i + k);
else if (j == (int)vec.size() - 1) to[i] = min(n, i + k);
else {
int t1 = (vec[j + 1] - vec[j] + k - 1) / k;
int t2 = (vec[j + 1] - i + k - 1) / k;
to[i] = (t1 < t2 ? vec[j] : min(n, i + k));
}
}
for ( int i = 1; i <= n; i ++)
f[i][0] = to[i], g[i][0] = (s[to[i]] == '0');
for ( int i = n; i >= 1; i --)
for ( int j = 1; j < 20; j ++) {
f[i][j] = f[f[i][j - 1]][j - 1];
g[i][j] = g[i][j - 1] + g[f[i][j - 1]][j - 1];
}
while (q --) {
int l, r, ans1 = 0, ans2 = 0;
cin >> l >> r;
if (l > r) swap(l, r);
for ( int i = 19; i >= 0; i --)
if (f[l][i] < r)
ans1 += g[l][i], ans2 += (1 << i), l = f[l][i];
if (! t) cout << ans1 << '\n';
else cout << ans1 << ' ' << ans2 + 1 << '\n';
}
return 0;
}
CF1251E2 Voting (Hard Version)
考虑从大的 \(m_i\) 开始考虑。
设当前 \(m_i=x\),记 \(m_i\lt x\) 的个数为 \(tot\),记在 \(\ge x\) 的人中买了 \(cnt\) 人。
那么如果满足 \(x\le cnt+tot\),就可以不用管了,不然就应该在所有 \(m_i\ge x\) 的人中继续买,直到满足这个条件。
肯定每次买都买最小的,用一个优先队列存一下就好。
正确性是很对的,因为已经默认了 \(m_i\lt x\) 的人全部都能被满足,所以 \(m_i=x\) 的人不能被满足,肯定只能从 \(m_i\ge x\) 的人中买。
代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 10;
int n;
vector< int> vec[N];
void solve() {
cin >> n;
for ( int i = 0; i < n; i ++) vec[i].clear();
for ( int i = 1; i <= n; i ++) {
int m, p; cin >> m >> p;
vec[m].push_back(p);
}
priority_queue< int> q;
int cnt = 0, sum = n, ans = 0;
for ( int i = n - 1; i >= 0; i --) {
if (vec[i].empty()) continue ;
for ( auto v : vec[i]) q.push(-v);
sum -= vec[i].size();
int need = i - sum;
while (cnt < need) {
ans += -q.top();
q.pop();
cnt ++;
}
}
cout << ans << '\n';
}
signed main() {
ios :: sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while (T --) solve();
return 0;
}
#P1356. 食物
如果直接维护整除的,需要枚举所有倍数,但是显然可以卡掉,所以考虑维护不能被整除的个数。
记 \(f_i\) 表示 \(1\sim i\) 中没有被集合中的质数整除,答案就是 \(n-f_n\)。
当加入一个数 \(p\) 时,\(f_i\) 该如何变化?要在 \(f_i\) 中剔除所有整除 \(p\) 的数,其实也就是 \(f_{\lfloor\dfrac{i}{p}\rfloor}\) 的值。
那么从大到小枚举 \(i\),更新即可。
删除一个数同理,给 \(f_i\) 加上 \(f_{\lfloor\dfrac{i}{p}\rfloor}\),从小到大更新。
复杂度是 \(O(nq)\),如何优化?
因为每次只询问 \(f_n\) 的值,不难发现或者打表可以得到只用维护 \(f_{\lfloor\dfrac{n}{i}\rfloor}\) 的值即可。
那么优化到 \(O(\sqrt{n}q)\)。
代码
#include <bits/stdc++.h>
#define all(x) (x).begin(), (x).end()
void Freopen() {
freopen("food.in", "r", stdin);
freopen("food.out", "w", stdout);
}
using namespace std;
const int N = 1e6 + 10;
int n, q;
int f[N];
int vis[N];
vector< int> vec1, vec2;
void add( int x) {
for ( int v : vec2) f[v] -= f[v / x];
}
void del( int x) {
for ( auto v : vec1) f[v] += f[v / x];
}
signed main() {
Freopen();
ios :: sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> q;
iota(f + 1, f + n + 1, 1);
for ( int i = 1; i <= n; i ++)
if (n / (i + 1) != n / i) vec1.push_back(i);
vec2 = vec1;
reverse(all(vec2));
while (q --) {
int x; cin >> x;
if (! vis[x]) {
add(x);
vis[x] = 1;
} else {
del(x);
vis[x] = 0;
}
cout << n - f[n] << '\n';
}
return 0;
}

浙公网安备 33010602011771号