省选联考 2024
省选联考 2024
前言
有的题没必要一定要推到满分才可以,比较阴间的写个八九十的分就很不错了,特别阴间的写个暴力就算了,没必要一定要全学懂是不是/fad
[省选联考 2024] 季风
讲题目转化为在 \((0,0)\),求最小 \(m\) 使 \(|x-\sum\limits_{i=0}^{m-1}x_{i\mod n}|+|y-\sum\limits_{i=0}^{m-1}y_{i\mod n}|\le k\times m\)
令 \(m=n\times z+i(0≤i<n)\),\(x_i\) 的前缀和 \(X_i\),\(y_i\) 的前缀和 \(Y_i\)
原式转化为 \(|x-X_n\times z-X_i|+|y-Y_n\times z-Y_i|\le k\times(n\times z+i)\)
解绝对值不等式,更新答案即可。
void calc(int k, int b)
{
if (k == 0 && b < 0) l = inf + 1;
else if (k > 0) r = min(r, (int)floor(1.0 * b / k));
else l = max(l, (int)ceil(1.0 * b / k));
}
signed main()
{
int T;
cin >> T;
while (T--)
{
cin >> n >> k >> x >> y;
for (rint i = 1; i <= n; i++)
{
cin >> X[i] >> Y[i];
X[i] += X[i - 1];
Y[i] += Y[i - 1];
}
if (!x && !y)
{
puts("0");
continue;
}
int ans = inf;
for (rint i = 1; i <= n; i++)
{
l = 0, r = inf;
calc(X[n] + Y[n] - n * k, x + y - X[i] - Y[i] + i * k);
calc(X[n] - Y[n] - n * k, x - y - X[i] + Y[i] + i * k);
calc(Y[n] - X[n] - n * k, y - x - Y[i] + X[i] + i * k);
calc(-X[n] - Y[n] - n * k, -x - y + X[i] + Y[i] + i * k);
if (l <= r) ans = min(ans, n * l + i);
}
cout << (ans == inf ? -1 : ans) << endl;
}
return 0;
}
[省选联考 2024] 魔法手杖
PS:窝太菜了,调了半天还是 96pts,算法应该没有假,应该是代码实现哪里出了问题,懒得调了。
题目大概就是说给你 \(n\) 个 \([0,2^k)\) 中的整数 \(a_1,a_2,\dots,a_n\),第 \(i\) 个数对应代价 \(b_i\)。
需要选取一个 \(U=\{1,2,\dots,n\}\) 的子集 \(S\),满足 \(\sum_{i\in S} b_i\le m\),以及一个 \([0,2^k)\) 中的整数 \(x\),最大化 \(\min\{\min_{i\in S}\{a_i+x\},\min_{i\in U\setminus S}\{a_i\oplus x\}\}\)。
最大化min,异或,\(2^k\) 对于这些东西要敏感,直接考虑二分和 dp 并不可做,考虑 01Trie 是否有操作空间
好像能玩,吃碗泡面开始把玩
从高往低逐位找 \(x\) 以及最终的答案
设当前走到了树上的某个结点 \(u\),已定答案 \(k\) 且只有高位有值,\(k\) 是从根走到 \(u\) 的路径异或上 \(x\) 对应的二进制数。
判断 \(k\) 单个位数,\(k\) 的当前位是否能为 \(1\),能的话最好。讨论 \(k\) 有两个儿子的情形,一个儿子是类似的。若希望 \(k\) 的当前位为 \(1\),须将 \(u\) 一个儿子子树内的所有数都划分进加法部分(该操作简记为划分),加法部分对答案的贡献也 \(\ge k\mid 2^K\),其中 \(2^K\) 为当前位的权值。
枚举要划分哪个儿子并判断是否可行,需要儿子子树内所有数对应的 \(b_i\) 之和不超过剩余可用的代价,其次加法部分的数的最小值 \(minn\) 需要满足 \(minn+(x\mid (2^d-1))\ge k\mid 2^d\),\(x\) 的值任意,将未确定的低位全部置为了 \(1\)。如果发现划分某一个儿子可以使得答案的当前位为 \(1\),就对应的更新 \(k\) 与 \(x\),朝另一个儿子递归。
若确定了 \(k\) 的当前位不能为 \(1\),仍考虑是否要划分一棵子树并令异或部分的当前位为 \(1\),此时异或部分不会对最终的答案产生限制,只考虑加法,最优解是将剩下低位全是 \(1\),所以用算出的数更新全局答案即可。不划分子树的情况,枚举 \(x\) 的取值,此时当前位异或为 \(1\) 的子树不会对答案产生限制,朝异或为 \(0\) 的子树递归即可。
复杂度 \(O (nk)\)
int n, m, k;
int b[N], ch[M][2], tot;
int a[N], minn[M];
int res;
int s[M];
int min(int a, int b)
{
return a < b ? a : b;
}
int max(int a, int b)
{
return a > b ? a : b;
}
inline int read()
{
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-')
f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
x = x * 10 + ch - '0', ch = getchar();
return x * f;
}
void print(int n)
{
if (n < 0)
{
putchar('-');
n *= -1;
}
if (n > 9) print(n / 10);
putchar(n % 10 + '0');
}
void insert(int x, int t)
{
int p = 0;
for (rint i = k - 1; i >= 0; i--)
{
if (!ch[p][x >> i & 1])
{
ch[p][x >> i & 1] = ++tot;
minn[tot] = inf;
s[tot] = 0;
}
p = ch[p][x >> i & 1];
minn[p] = min(minn[p], x);
s[p] += t;
}
}
//mr -> min
void solve(int p, int d, int mr, int w, int x, int ans)
{
if (!d)
{
res = max(res, min(mr + x, ans));
return ;
}
int tag = 1;
tag <<= d - 1;
if (!ch[p][0] || !ch[p][1])
{
if (ch[p][0]) solve(ch[p][0], d - 1, mr, w, x | tag, ans | tag);
if (ch[p][1])
{
if (x + mr > ans) solve(ch[p][1], d - 1, mr, w, x, ans | tag);
else
{
res = max(res, x + mr + tag - 1);
solve(ch[p][1], d - 1, mr, w, x | tag, ans);
}
}
return ;
}
bool flag = 0;
if (w + s[ch[p][0]] <= m)
{
if (min(mr, minn[ch[p][0]]) + x > ans) solve(ch[p][1], d - 1, min(mr, minn[ch[p][0]]), w + s[ch[p][0]], x, ans | tag), flag = 1;
else res = max(res, min(mr, minn[ch[p][0]]) + x + tag - 1);
}
if (w + s[ch[p][1]] <= m)
{
if (min(mr, minn[ch[p][1]]) + x + tag > ans) solve(ch[p][0], d - 1, min(mr, minn[ch[p][1]]), w + s[ch[p][1]], x | tag, ans | tag), flag = 1;
else res = max(res, min(mr, minn[ch[p][1]]) + x + tag + tag - 1);
}
if (!flag)
{
solve(ch[p][0], d - 1, mr, w, x, ans);
solve(ch[p][1], d - 1, mr, w, x | tag, ans);
}
}
signed main()
{
int useless, T;
long times = 0;
useless = read();
T = read();
while (T--)
{
times++;
for (rint i = 0; i <= tot; i++) ch[i][0] = ch[i][1] = 0;
res = 0;
tot = 0;
n = read();
m = read();
k = read();
int min_ = inf;
int sum = 0;
for (rint i = 1; i <= n; i++)
{
a[i] = read();
min_ = min(min_, a[i]);
}
for (rint i = 1; i <= n; i++)
{
b[i] = read();
sum += b[i];
}
if (sum <= m)
{
res = min_ + (1 << k) - 1;
}
for (rint i = 1; i <= n; i++) insert(a[i], b[i]);
solve(0, k, inf, 0, 0, 0);
print(res);
cout << endl;
}
return 0;
}
[省选联考 2024] 迷宫守卫
PS:之所以直接跳到了 Day 2 T1 是因为 Day 1 T3 暴力都不会打。
题意要求满二叉树上最大化最小叶子点权字典序
显然不是 01Trie,考虑 二分和 dp 是否有操作空间
能玩,而且可做性比 Day 1 T2 高一些,剩下的泡面吃两口开始把玩
对比 Day 1 T2,思路是找当前为能否设置为 1,同样都是最大化最小什么的东西,考虑同样的思路,转化为判断能否使第一位是某个数的判定性问题
把大于等于某个数 \(x\) 的数视为 \(1\),视为 \(0\),判断是否能在魔力值够的前提下使最小字典序序列的第一位为 \(1\)。
设 \(g_i\) 为使以 \(i\) 为根的子树的最小字典序序列的第一位为 \(1\) 的最小代价,显然有转移 \(g_i=g_{i\times2}+\min(g_{i\times2+1},w_i)\)
对第一位进行操作,二分这个 \(x\),在最小字典序序列的第一位可以为 \(1\) 的前提下,\(x\) 取到最大值时,最小字典序序列的第一位一定是 \(x\)
同一个子树的最小字典序序列中的数,在最终的最小字典序序列中也一定在一起 。从 \(q_k=x\) 的结点 \(k\) 出发,一路向父亲走,走到的结点的子树所对应的最小字典序序列,在最终的最小字典序序列中排在最前面。每个走到的节点 \(u\) 的父亲的另一个儿子 \(j_u\) 的子树所对应的最小字典序序列,在最终的最小字典序序列中按照走到的顺序依次从前向后排。按照这个顺序,对于所有上述的 \(j\) 的子树,依次推倒重来,即进行上面的过程
对一些子树推倒重来时可能改变左右子树最小字典序序列的大小关系,需要考虑这些情况,保证 Bob 第一个到达的叶结点一定是 \(k\),以及不浪费魔力值。如果一个走到的节点 \(i\) 是一个右儿子,那么它的子树对应的最小字典序序列一定小于 \(j_i\) 的,因为消耗更多魔力值改变方案只会变得更大,所以可以将 \(j_i\) 的方案直接推倒重来;如果 \(i\) 是一个左儿子,那么当 \(g_{j_i}≤w_{fa_i}\)时,也可以直接将 \(j_i\) 的方案直接推倒重来;否则,如果目前方案剩余的魔力值足以从激活父亲的石像守卫改为使 \(j_i\) 的子树的最小字典序序列的第一位视为 \(1\),那么就不激活父亲的石像守卫,不然仍然需要激活;最后将 \(j_i\) 的方案推倒重来。
int check(int x, int k)
{
if ((x << 1) >= n)
{
f[x] = (a[x << 1] < k ? inf : (a[(x << 1) + 1] < k ? a[x] : 0));
}
else
{
check(x << 1, k);
check((x << 1) + 1, k);
f[x] = min(inf, f[x << 1] + min(a[x], f[(x << 1) + 1]));
}
return f[x];
}
void dfs(int x)
{
int l = 1;
int r = n;
while (l < r)
{
int mid = (l + r + 1) >> 1;
if (check(x, mid) <= w) l = mid;
else r = mid - 1;
}
check(x, l);
l = rev[l]; w -= f[x];
ans[++cnt] = a[l]; ans[++cnt] = a[l ^ 1];
int i = l >> 1;
while (i != x)
{
if (i & 1) w += f[i ^ 1];
else if (f[i ^ 1] <= a[i >> 1]) w += f[i ^ 1];
else if (w >= f[i ^ 1] - a[i >> 1]) w += a[i >> 1];
dfs(i ^ 1);
i >>= 1;
}
}
signed main()
{
int t;
cin >> t;
while (t--)
{
cin >> m >> w;
n = 1 << m;
cnt = 0;
for (rint i = 1; i < 2 * n; i++) cin >> a[i];
for (rint i = n; i < 2 * n; i++) rev[a[i]] = i;
dfs(1);
for (rint i = 1; i <= n; i++) cout << ans[i] << " ";
cout << endl;
}
return 0;
}
[省选联考 2024] 重塑时光
对于 85pts 部分的 dp 还是比较好弄得,但是剩下优化到可以卡进去的代码我是不会写的,看别人的题解自己写还算不出来,写到 85pts 算了。
设 \(f_S\) 表示集合为 \(S\) 的点放在一段,内部排序有多少的方案数。
设 \(h_{i,S}\) 表示集合为 \(S\) 的点分成 \(i\) 段,使得这 \(i\) 段中两两没有边。dp 时枚举超集转移即可。
设 \(g_{i,S}\) 表示现在加了 \(i\) 个非空段,且它们由集合 \(S\) 里的点组成且合法的方案数。这 \(i\) 个非空段构成一个 DAG 的时候是合法的,所以我们考虑每次假如 \(j\) 个入度为 \(0\) 的非空段,让它们与前面的段连边。但我们发现这样会重复。于是考虑更改转移意义为加入 \(j\) 个入度为 \(0\) 的非空段,使得入度为 \(0\) 的至少为 \(j\) 个,容斥一下,得出转移
\(g_{i,S}=\sum_{j=1,T} (-1)^{j+1}\times g_{i-j,S-T}\times h_{j,T}\)
复杂度 \(O(3^nn^2)\)
int qpow(int a, int b = mod - 2, int p = mod)
{
int res = 1;
while (b)
{
if (b & 1) res = res * a % p;
a = a * a % p;
b >>= 1;
}
return res;
}
bool v(int s, int t)
{
return adj[s] & t;
}
signed main()
{
int n, m, k;
cin >> n >> m >> k;
for (rint i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
u--;
v--;
for (rint s = 0; s < (1 << n); s++)
if (s & (1 << u))
adj[s] |= 1 << v;
}
for (rint i = 0; i < 32; i++)
for (rint j = c[i][0] = 1; j <= i; j++)
c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
fac[0] = ifac[0] = 1;
for (rint i = 1; i < 32; i++) ifac[i] = qpow(fac[i] = fac[i - 1] * i % mod);
h[0] = 1;
for (rint s = 1; s < (1 << n); s++)
for (rint i = 0; i < n; i++)
if ((s & (1 << i)) && !v(1 << i, s ^ (1 << i)))
h[s] = (h[s] + h[s ^ (1 << i)]) % mod;
g[0][0] = 1;
for (rint i = 1; i <= n; i++)
for (rint s = 1; s < (1 << n); s++)
for (rint t = s; t; t = (t - 1) & s)
if (!v(t, s ^ t) && !v(s ^ t, t))
g[i][s] = (g[i][s] + g[i - 1][s ^ t] * h[t]) % mod;
for (rint i = 1; i <= n; i++)
for (rint s = 1; s < (1 << n); s++)
{
g[i][s] = g[i][s] * ifac[i] % mod;
if (~i & 1) g[i][s] = mod - g[i][s];
}
f[0][0] = 1;
for (rint i = 1; i <= n; i++)
for (rint s = 1; s < (1 << n); s++)
for (rint t = s; t; t = (t - 1) & s)
if (!v(s ^ t, t))
for (rint j = i; j; j--)
f[i][s] = (f[i][s] + f[i - j][s ^ t] * g[j][t]) % mod;
int ans = 0;
for (rint i = 1; i <= min(k + 1, n); i++)
ans = (ans + f[i][(1 << n) - 1] * fac[i] % mod * c[k + 1][i]) % mod;
cout << ans * fac[k] % mod * ifac[n + k] % mod << endl;
return 0;
}
[省选联考 2024] 最长待机
视一条长度为 \(y\) 的全是 \(1\) 的链为 \(x^y\)
结论:
- \(x^y = x \times (nx^{y-1})\)(\(n\) 为常数)
- \(x^y+x^{y-1}=(x+1) \times x^{y-1}=x^y\)
- \(x^{y-1}+x^y>x^y\),因为左式的 \(x^{y-1}\) 和右式的 \(x^y\) 同时输入,所以可以保证左式的 \(x^y\) 比右式大,不能忽略。
对询问的情况分类讨论:
- \(e_{k}=1\),所以此时的答案和 \(k\) 所在的子树中,与 \(k\) 的 \(1\) 最多的链的 \(1\) 的数量有关。
- \(e_{k}=0\),此时我们从 \(k\) 向下遍历直到遍历到第一个 \(1\) 或者叶子结点的 \(0\),把它的答案(根据 \(e_k = 1\) 部分的答案)从左到右排序,形成一个序列,它的答案就是不能被忽略的数的和,即那些左边没有数字比它大的数的和。
复杂度 \(O(nm)\) 可以直接写
int bfs(int x, int depth)
{
int res = depth;
for (auto y : e[x]) res = max(res, bfs(y, depth + a[y]));
return res;
}
void dfs(int x)
{
if (a[x])
{
v.push_back(bfs(x, 1));
return ;
}
else if (e[x].empty())
{
v.push_back(0);
}
for (auto y : e[x]) dfs(y);
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= n; i++)
{
int k;
cin >> a[i] >> k;
for (rint j = 1; j <= k; j++)
{
int x;
cin >> x;
e[i].push_back(x);
}
}
while (m--)
{
cin >> op >> x;
if (op == 1)
{
a[x] ^= 1;
}
if (op == 2)
{
v.clear();
dfs(x);
if (v.size() == 1)
{
cout << v[0] + (v[0] == 0) << endl;
}
else
{
int last = -1, ans = 0, idx = 0;
for (auto i : v)
{
if (i >= last)
{
if (i == 0) ans++;
else ans += i;
idx++;
last = i;
}
}
cout << ans + (idx != 1) << endl;
}
}
}
return 0;
}
之后用线段树优化不会写,可以停了。
后记
怎么说呢,现在 HE 已经是弱省了,除了 Day 1 T1 要稳过以外别的题暴力打满能拿得分都拿到就好了。现在 CCF 出题方向不再是考阴间的算法而是在于基础,这次省选考的算法其实都是 NOIP 大纲里的,如果可以把 NOIP 范围内的算法做到炉火纯青打省选其实一点问题都没有。
简言之,在保证自己能吃的泡面都吃到的基础上再去考虑能不能加餐。