2022牛客寒假算法基础集训营
这里是牛客寒假比赛部分题目的简析,主要挑选的是比赛时做的时间长的题目或没有做出来的题目又或者是感觉比较巧妙的题目(
第一场
A
考虑数字根的本质,一个 \(m\) 位十进制数第 \(i\) 位若用 \(a_i\) 表示,则求数字根可以表示为:
特别的,当此结果为 0 时,数字根为 9。
然后就是很平常的背包 dp 了。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
const int p = 998244353;
const int maxn = 1e5 + 10;
int n, a[maxn], f[maxn][10];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
f[0][0] = 1;
for (int i = 1; i <= n; ++i)
{
a[i] %= 9;
for (int j = 0; j < 9; ++j)
{
if (a[i] > j)
{
f[i][j] = (f[i][j] + f[i - 1][j - a[i] + 9]) % p;
}
else
{
f[i][j] = (f[i][j] + f[i - 1][j - a[i]]) % p;
}
f[i][j] = (f[i][j] + f[i - 1][j]) % p;
}
}
for (int i = 1; i < 9; ++i)
printf("%d ", f[n][i]);
printf("%d", (f[n][0] + p - 1) % p);
return 0;
}
B
发现答案具有区间可加性,故预处理出 \(f(i, j, k)\) 表明 \([i, i + 2^j - 1]\) 这个子串,初始分模 3 为 \(k\) 的答案。每一次查询只需二进制分解区间长度,每次往后跳 2 的整次幂个位置即可。
时间复杂度为 \(O(n \log n + q \log n)\)。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
const int maxn = 2e5 + 10;
int n, q, f[maxn][21][3], log_2[maxn];
char s[maxn];
int main()
{
scanf("%d%d%s", &n, &q, s + 1);
log_2[0] = -1;
for (int i = 1; i <= n; ++i)
{
log_2[i] = log_2[i >> 1] + 1;
f[i][0][0] = (s[i] == 'W');
f[i][0][1] = (s[i] == 'W') - (s[i] == 'L');
f[i][0][2] = (s[i] == 'W') - (s[i] == 'L');
}
log_2[0] = 0;
for (int j = 1, v; (1 << j) <= n; ++j)
{
for (int i = 1; i + (1 << (j - 1)) <= n; ++i)
{
v = (f[i][j - 1][0] % 3 + 3) % 3;
f[i][j][0] = f[i][j - 1][0] + f[i + (1 << (j - 1))][j - 1][v];
v = (f[i][j - 1][1] % 3 + 4) % 3;
f[i][j][1] = f[i][j - 1][1] + f[i + (1 << (j - 1))][j - 1][v];
v = (f[i][j - 1][2] % 3 + 5) % 3;
f[i][j][2] = f[i][j - 1][2] + f[i + (1 << (j - 1))][j - 1][v];
}
}
int len, l, r, s;
while (q--)
{
scanf("%d%d%d", &l, &r, &s);
len = r - l + 1;
for (int i = log_2[len]; i >= 0; --i)
{
if ((len >> i) & 1)
{
s = s + f[l][i][s % 3];
l += (1 << i);
}
}
printf("%d\n", s);
}
return 0;
}
G
由于是局部最小值,所以我们只需关注相对大小,则变换可以视作 \(f_i = \left\vert f_i - b \right\vert\)。而绝对值可以视为 \(f_i\) 与 \(b\) 在数轴上的距离。
画图表明,对于 \(f_{i - 1}, f_i, f_{i + 1}\) 使得 \(f_i\) 变为区间最小值的 \(b\)。一定是连续的一段,故我们将此问题转化为了一个线段覆盖问题。我们只需通过差分,对于每个位置计算出覆盖在该位置上的线段个数后,取最小值即可。
由于值域过大,我们需要开一个 map 代替数组进行差分。
Code
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <map>
using std::min;
using std::max;
const int inf = 1e9;
const int maxn = 1e5 + 10;
int n, f[maxn];
std::map<int, int> d;
inline void solve()
{
d.clear();
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", f + i);
int cur = 0;
for (int i = 2, l, r; i < n; ++i)
{
if (f[i] == f[i - 1] || f[i] == f[i + 1])
continue;
if (f[i] < min(f[i - 1], f[i + 1]))
{
++cur;
r = (f[i] + min(f[i - 1], f[i + 1]) + 1) / 2 - 1;
--d[r + 1];
}
else if (f[i] > max(f[i - 1], f[i + 1]))
{
l = (f[i] + max(f[i - 1], f[i + 1])) / 2 + 1;
++d[l];
continue;
}
else
{
l = (f[i] + min(f[i - 1], f[i + 1])) / 2 + 1;
r = (f[i] + max(f[i - 1], f[i + 1]) + 1) / 2 - 1;
if (l <= r)
++d[l], --d[r + 1];
}
}
int ans = cur;
for (auto &[i, val] : d)
{
cur += val;
ans = min(ans, cur);
}
printf("%d\n", ans);
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
solve();
return 0;
}
H
题目中计算的式子有绝对值,只需考虑将绝对值拆开,分开计算贡献即可。即考虑所有数对 \((a_i, a_j)\),如果 \(a_i + a_j \ge 1000\) 则该数对中所有数做一次正贡献,1000 做一次负贡献,反之亦然。
故对于 \(a_i\) 只需考虑大于等于 \(1000 - a_i\) 的 \(a_j\) 有几个。有几个就可对应做几次正贡献,剩下不满足条件的 \(a_j\) 一起做负贡献。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
const int maxn = 1e6 + 10;
int n, a[maxn], cnt[1010], sum[1010];
int main()
{
scanf("%d", &n);
long long tot = 0;
for (int i = 1; i <= n; ++i)
{
scanf("%d", a + i);
tot += a[i];
++cnt[a[i]];
sum[a[i]] += a[i];
}
for (int i = 1; i <= 1000; ++i)
{
sum[i] += sum[i - 1];
cnt[i] += cnt[i - 1];
}
long long ans = 0;
for (int i = 1; i <= n; ++i)
{
ans += 1000 * (cnt[1000 - a[i]] * 2ll - n);
ans += a[i] * (n - 2ll * cnt[1000 - a[i]]) + tot - 2ll * sum[1000 - a[i]];
}
for (int i = 1; i <= n; ++i)
ans -= abs(2 * a[i] - 1000);
ans /= 2;
for (int i = 1; i <= n; ++i)
ans += abs(2 * a[i] - 1000);
printf("%lld\n", ans);
return 0;
}
K
设 \(f(i, j, k, l)\) 表明考虑前 \(i\) 个岛,且第 \(i - 2, i - 1, i\) 个岛的颜色分别为 \(j, k, l\) 时,绿岛最大可能个数。(我们用 0、1、2 依次表示绿、红、黑)
转移就非常显然了:\(f(i, k, l, p) = \max_{j = 0}^2 \limits f(i - 1, j, k, l) + [p = 0]\)。为了保证转移满足罗盘的限制,我们只需统计 \((k, l, p)\) 中红色和绿色的个数,然后判断其是否满足条件即可。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
const int maxn = 1e5 + 10;
int n, f[maxn][3][3][3];
char s[maxn];
int main()
{
scanf("%d%s", &n, s + 1);
memset(f, -1, sizeof(f));
int r, g;
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 3; ++j)
{
for (int k = 0; k < 3; ++k)
{
g = (i == 0) + (j == 0) + (k == 0);
r = (i == 1) + (j == 1) + (k == 1);
if (s[3] == 'G' && g > r)
f[3][i][j][k] = max(f[3][i][j][k], g);
if (s[3] == 'R' && g < r)
f[3][i][j][k] = max(f[3][i][j][k], g);
if (s[3] == 'B' && g == r)
f[3][i][j][k] = max(f[3][i][j][k], g);
}
}
}
for (int i = 4; i <= n; ++i)
{
for (int j = 0; j < 3; ++j)
{
for (int k = 0; k < 3; ++k)
{
for (int l = 0; l < 3; ++l)
{
if (f[i - 1][j][k][l] == -1)
continue;
for (int p = 0; p < 3; ++p)
{
g = (p == 0) + (k == 0) + (l == 0);
r = (p == 1) + (k == 1) + (l == 1);
if (s[i] == 'G' && g > r)
f[i][k][l][p] = max(f[i][k][l][p], f[i - 1][j][k][l] + (p == 0));
if (s[i] == 'R' && g < r)
f[i][k][l][p] = max(f[i][k][l][p], f[i - 1][j][k][l] + (p == 0));
if (s[i] == 'B' && g == r)
f[i][k][l][p] = max(f[i][k][l][p], f[i - 1][j][k][l] + (p == 0));
}
}
}
}
}
int ans = -1;
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 3; ++j)
{
for (int k = 0; k < 3; ++k)
ans = max(ans, f[n][i][j][k]);
}
}
printf("%d\n", ans);
return 0;
}
第二场
A
不难发现,最多只能出 \(\min(n, m + 1)\) 次法术进攻牌。
我们发现可以恰好造成的伤害,大体上是一段连续的区间。我们考虑如何证明这个结论,我们发现对于一个出牌序列,如果我们交换相邻两张法术进攻牌和法术回复牌的次序,可以使最终总的攻击力变化 1。故不难想到这样的策略:
-
一开始,将 \(m\) 张法术回复牌放入出牌序列,在序列开头加入一张法术回复牌,此时攻击力为 1。
-
通过交换相邻法术进攻牌和法术回复牌,使得序列总体的攻击力增长 1,直到其移动至序列末尾,做出 \(m + 1\) 的战斗力贡献。
-
若序列中现存的 \(k\) 张法术进攻牌都移动至序列末尾后,如果此时在开头添加一张法术进攻牌,会使总攻击力增加 \(k + 1\),我们现在想通过变化插入前的序列,使得添加这张新的牌后总攻击力仍增加 1。故考虑将末尾的一张法术进攻牌插入到后 \(k - 1\) 张法术进攻牌之前,再将另一张末尾的法术进攻牌与最后一张法术进攻牌交换位置,此时在添加一张法术牌,总攻击力增加 1。而显然当 \(m = 1, k = 1\) 和 \(m = 2, k = 2\) 时,无法实现此种构造,且我们可以通过直接枚举情况得知:当 \(m = 1\) 时无法恰好造成 2 点伤害,当 \(m = 2\) 时无法恰好造成 8 点伤害。故这两种情况需特判无解。
-
我们只需依次重复 2. 3. 使得最终 \(\min(n, m + 1)\) 张法术进攻牌都移动至序列末尾即可。
通过以上构造也可以看出,恰好造成的伤害最大为 \(\dfrac{m \times (2m + 1 + \min(n, m + 1))}{2}\)。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
int n, m, q;
int main()
{
scanf("%d%d%d", &n, &m, &q);
n = std::min(n, m + 1);
ll x, bound;
bound = (2ll * m + n + 1) * n / 2;
while (q--)
{
scanf("%lld", &x);
if (m == 1 && x == 3)
puts("NO");
else if (m == 2 && x == 8)
puts("NO");
else if (x <= bound)
puts("YES");
else
puts("NO");
}
return 0;
}
B
考虑如何进行较少次数的操作 2,对于两个结点 \(u, v\) 来说,如果它们可以连通,且 \(a_u < a_v\) 则我们可以先将 \(v\) 加到 \(a_v - a_u\),然后通过操作 1 两个结点连通变成再共同加 \(a_u\)。
所以我们可以考虑每次让两个权值较大的结点合并到一个连通块中,直到生成一棵树。这也保证我们操作 1 不会使用多余 \(\min(5n, m)\) 次。
具体来说我们可以按权值从大到小依次枚举结点,将当前结点与和它相连且比其权值大的结点合并到一个连通块即可。此时当前结点的贡献为原来连通块中结点的最小权值与当前结点权值的差。最后不要忘记计算每个连通块内权值最小的结点。上述合并过程我们可以使用并查集维护,并且我们为了使得并查集中代表连通块的结点总是连通块中权值最小的,我们可以使用只使用路径压缩优化的并查集简化代码。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
const int maxn = 5e5 + 10;
int n, m, a[maxn], head[maxn], to[maxn * 20], next[maxn * 20], tot;
int id[maxn], par[maxn];
bool vis[maxn];
int find(int x)
{
return par[x] == x ? x : par[x] = find(par[x]);
}
inline void add_edge(int u, int v)
{
to[++tot] = v;
next[tot] = head[u];
head[u] = tot;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
{
scanf("%d", a + i);
id[i] = i, par[i] = i;
}
std::sort(id + 1, id + n + 1, [&](int x, int y){return a[x] > a[y];});
for (int i = 1, u, v; i <= m; ++i)
{
scanf("%d%d", &u, &v);
add_edge(u, v), add_edge(v, u);
}
long long ans = 0;
for (int i = 1, u; i <= n; ++i)
{
u = id[i];
vis[u] = true;
for (int c = head[u], v, x, y; c; c = next[c])
{
v = to[c];
x = find(u), y = find(v);
if (vis[v] && x != y)
{
ans += a[y] - a[u];
par[y] = x;
}
}
}
for (int i = 1; i <= n; ++i)
{
if (find(i) == i)
ans += a[i];
}
printf("%lld\n", ans);
return 0;
}
E
最小值的构造可以由“竞赛图缩点后是一条链”的结论直接得出,故最小值为 \(n - 1\)。
而对于最大值来说,题目中说每条边只能走一次,可以联想到欧拉路。
考虑 \(n\) 为奇数的情况,显然由于每个结点都是偶度数结点,故图中存在一条欧拉回路,答案为 \(\dfrac{n(n - 1)}{2}\)。
考虑 \(n\) 为偶数的情况,由于每个结点都是奇度数结点,故考虑在如何图中删除最少的边使得图中存在一条欧拉路。我们可以将图中的结点分成 \(\dfrac{n}{2}\) 对,然后删除每一对结点之间相连的边,这样图中的每个结点就都变成了偶度数结点,故存在一条欧拉回路,如果少删一条边,则图中会只有两个奇度数结点,那么图中就存在一条欧拉路,故 \(n\) 为偶数时,答案为 \(\dfrac{n(n - 2)}{2} + 1\)。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
ll n;
int main()
{
scanf("%d", &n);
if (n & 1)
printf("%lld %lld\n", n - 1, n * (n - 1) / 2);
else
printf("%lld %lld\n", n - 1, n * (n - 2) / 2 + 1);
return 0;
}
G
本题可以有一个结论是从 \(u\) 前往 \(v\) 和从 \(v\) 前往 \(u\) 的答案一样,证明只需考虑正着走过“两边高,中间低”的一段木桩的答案和反着走过一段木桩的答案相等即可。
事实上,我们不需要这个结论也可以解决本题。发现在木桩上从一个结点跳到另一个结点所需要的体力与一开始跳上木桩消耗的体力无关。所以我们可以重链剖分后,用前缀和的方式预处理每条重链上的每一个结点跳到链顶所需耗费的体力。对于一次询问 \((u, v)\),我们只需求出 \(\operatorname{lca}(u, v)\) 后,将 \((u, v)\) 分解成 \((u, \operatorname{lca}(u, v)), (\operatorname{lca}(u, v), v)\) 即可。
由于我们每次跳链统计答案的时间复杂度为 \(O(1)\),所以单次查询时间复杂度为 \(O(\log n)\)。总时间复杂度为 \(O(n + m \log n)\)。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
const int maxn = 1e6 + 10;
int n, m, a[maxn], dfn[maxn];
int head[maxn], to[maxn << 1], next[maxn << 1], tot, tim;
int depth[maxn], par[maxn], top[maxn], son[maxn], size[maxn];
ll up[maxn], down[maxn];
inline void add_edge(int u, int v)
{
to[++tot] = v;
next[tot] = head[u];
head[u] = tot;
}
void dfs_son(int u, int pre)
{
size[u] = 1, depth[u] = depth[pre] + 1, par[u] = pre;
for (int c = head[u], v; c; c = next[c])
{
v = to[c];
if (v == pre)
continue;
dfs_son(v, u);
size[u] += size[v];
if (size[son[u]] < size[v])
son[u] = v;
}
}
void dfs_top(int u, int anc)
{
top[u] = anc;
dfn[u] = ++tim;
if (!son[u])
return;
up[tim + 1] = up[tim] + max(0, a[u] - a[son[u]]);
down[tim + 1] = down[tim] + max(0, a[son[u]] - a[u]);
dfs_top(son[u], anc);
for (int c = head[u], v; c; c = next[c])
{
v = to[c];
if (v == par[u] || v == son[u])
continue;
dfs_top(v, v);
}
}
inline int lca(int x, int y)
{
while (top[x] != top[y])
{
if (depth[top[x]] < depth[top[y]])
swap(x, y);
x = par[top[x]];
}
return depth[x] < depth[y] ? x : y;
}
inline ll going_up(int x, int tar)
{
ll res = 0;
while (top[x] != top[tar])
{
res += up[dfn[x]] + max(0, a[par[top[x]]] - a[top[x]]);
x = par[top[x]];
}
res += up[dfn[x]] - up[dfn[tar]];
return res;
}
inline ll going_down(int x, int tar)
{
ll res = 0;
while (top[x] != top[tar])
{
res += down[dfn[x]] + max(0, a[top[x]] - a[par[top[x]]]);
x = par[top[x]];
}
res += down[dfn[x]] - down[dfn[tar]];
return res;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1, u, v; i < n; ++i)
{
scanf("%d%d", &u, &v);
add_edge(u, v), add_edge(v, u);
}
dfs_son(1, 0), dfs_top(1, 1);
int x, y, anc;
while (m--)
{
scanf("%d%d", &x, &y);
anc = lca(x, y);
printf("%lld\n", a[x] + going_up(x, anc) + going_down(y, anc));
}
return 0;
}
L / M
按照传统计数题目的套路,我们设 \(f(i)\) 表明以下标为 \(i\) 的气运为结尾,由前 \(i\) 个气运组成气运序列的方案数。
本题有两个维度,要求在 \(a_i\) 维度上选择的气运递增,在与 \(b_i\) 相关的下标维度上,如果要选第 \(i\) 个,则必须在下标 \([i - b_i, i)\) 中至少选了一个(除非是之前没有选择任何气运)。
由于数字递增的条件比较难以满足,故考虑先将气运对 \(a_i\) 从小到大排序,排序后我们发现对于 \(a'_i\),我们只能从所有以 \(a'_j\,(j < i)\) 为结尾的气运序列中选择一个继承状态。而我们选择要继承的 \(a'_j\) 又必须满足原来的下标在 \([i - b_i, i)\) 中。所以我们可以对下标这一维开一棵权值树状数组,维护 \(f(i)\) 的前缀和,以便快速实现 \(f(i) = 1 + \sum_{k = i - b_i}^{b_i - 1} f(k)\) 的转移。
时间复杂度 \(O(n \log n)\),树状数组常数较小,可以通过本题。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::max;
using std::min;
using std::pair;
using std::sort;
using std::swap;
using std::vector;
typedef long long ll;
namespace GenHelper
{
int z1, z2, z3, z4, z5, u, res;
int get()
{
z5 = ((z1 << 6) ^ z1) >> 13;
z1 = ((int)(z1 & 4294967) << 18) ^ z5;
z5 = ((z2 << 2) ^ z2) >> 27;
z2 = ((z2 & 4294968) << 2) ^ z5;
z5 = ((z3 << 13) ^ z3) >> 21;
z3 = ((z3 & 4294967) << 7) ^ z5;
z5 = ((z4 << 3) ^ z4) >> 12;
z4 = ((z4 & 4294967) << 13) ^ z5;
return (z1 ^ z2 ^ z3 ^ z4);
}
int read(int m)
{
u = get();
u >>= 1;
if (m == 0)
res = u;
else
res = (u / 2345 + 1000054321) % m;
return res;
}
void srand(int x)
{
z1 = x;
z2 = (~x) ^ (0x23333333);
z3 = x ^ (0x12345798);
z4 = (~x) + 51;
u = 0;
}
}
using namespace GenHelper;
const int N = 2e6 + 7, p = 1e9 + 7;
class BIT
{
private:
int v[N], bound;
inline int lowbit(int x)
{
return x & -x;
}
public:
inline void build(int n)
{
bound = n;
memset(v, 0, sizeof(v));
}
inline void add(int pos, int x)
{
for (int i = pos; i <= bound; i += lowbit(i))
v[i] = (v[i] + x) % p;
}
inline int query(int pos)
{
int res = 0;
for (int i = pos; i > 0; i -= lowbit(i))
res = (res + v[i]) % p;
return res;
}
inline int query(int ql, int qr)
{
return (query(qr) - query(ql - 1) + p) % p;
}
}tree;
int a[N], b[N], id[N], f[N];
int main()
{
int n, seed;
scanf("%d %d", &n, &seed);
srand(seed);
for (int i = 1; i <= n; i++)
{
a[i] = read(0), b[i] = read(i);
id[i] = i;
}
tree.build(n);
sort(id + 1, id + n + 1, [&](int x, int y){return a[x] == a[y] ? x < y : a[x] < a[y];});
for (int i = 1; i <= n; ++i)
{
f[id[i]] = tree.query(id[i] - b[id[i]], id[i]) + 1;
if (f[id[i]] >= p)
f[id[i]] -= p;
tree.add(id[i], f[id[i]]);
}
int ans = 0;
for (int i = 1; i <= n; ++i)
ans = (ans + f[i]) % p;
printf("%d\n", ans);
return 0;
}
第三场
C
考虑设购买了 \(i\) 个西瓜且重量总和为 \(j\) 的方案数为 \(f(i, j)\)。则有转移:
所以本题相当于给定 \(f(n, j)\) 求 \(w_i\)。
分析可知,如果 \(f(n, 1)\) 不为 0,则必然会有一个 \(w_i = 2\) 的瓜。考虑如何消除这个瓜的影响,我们只需根据状态转移方程倒着转移即可消除其影响,这时可以认为原来的 \(f(n, j)\) 都变为了 \(f(n - 1, j)\)。进而我们可以从小到大枚举 \(j\),我们忽略状态中的第一维,则只要 \(f(j)\) 不为 0,则存在一个大小为 \(w_i = 2j\) 的瓜。如此即可反推出可能的瓜的重量序列。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
const int p = 1e9 + 7;
int m, f[1010], w[1010];
int main()
{
scanf("%d", &m);
f[0] = 1;
for (int i = 1; i <= m; ++i)
scanf("%d", f + i);
int n = 0;
for (int i = 1; i <= m; ++i)
{
while (f[i])
{
w[++n] = i * 2;
for (int j = i; j <= m; ++j)
{
f[j] = (f[j] - f[j - i] + p) % p;
if (j >= i * 2)
f[j] = (f[j] - f[j - i * 2] + p) % p;
}
}
}
printf("%d\n", n);
for (int i = 1; i <= n; ++i)
printf("%d%c", w[i], " \n"[i == n]);
return 0;
}
F / H
不难发现以某一结点 \(u\) 为轴旋转即为一次单旋,旋转后 \(u\) 的深度减少 1。
所以我们可以从根开始遍历初始平衡树,如果此时初始平衡树和当前平衡树的根结点不同,则在当前平衡树中找到根节点,通过单旋将其不断转至根结点。现在两棵平衡树的根节点已经相同,我们现在还需要让两棵树根节点的左右子树分别相同,这样就得到了两个子问题,递归求解即可。
由于本题 \(n\) 只有 1000,所以我们可以暴力单旋,最终以 \(O(n^2)\) 的复杂度通过本题。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
int n, ch[1010][2], par[1010], lch[1010], rch[1010], fa[1010];
vector<int> ans;
inline int id(int u)
{
return ch[par[u]][1] == u;
}
inline void rotate(int u)
{
ans.push_back(u);
int v = par[u], w = par[v], d = id(u), key = ch[u][d ^ 1];
par[key] = v, ch[v][d] = key;
par[u] = w, ch[w][id(v)] = u;
par[v] = u, ch[u][d ^ 1] = v;
}
void dfs(int u)
{
while (par[u] != fa[u])
rotate(u);
if (lch[u])
dfs(lch[u]);
if (rch[u])
dfs(rch[u]);
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
{
scanf("%d%d", lch + i, rch + i);
if (lch[i])
fa[lch[i]] = i;
if (rch[i])
fa[rch[i]] = i;
}
for (int i = 1; i <= n; ++i)
{
scanf("%d%d", ch[i], ch[i] + 1);
if (ch[i][0])
par[ch[i][0]] = i;
if (ch[i][1])
par[ch[i][1]] = i;
}
int root = 0;
for (int i = 1; i <= n; ++i)
{
if (!fa[i])
root = i;
}
dfs(root);
printf("%zu\n", ans.size());
for (int i : ans)
printf("%d\n", i);
return 0;
}
I
我们显然可以发现以 \(r\) 这个位置右端点,以 \(l\) 这个位置为左端点所构成的子串 \([l, r]\) 中离 \(r\) 最近的位置 \(l\) 随 \(r\) 单调不减。
故我们可以直接考虑双指针,在 \(r\) 向后维护与其最近 \(l\) 的位置,密码长度的限制较容易维护,只需保证 \(r - l + 1 \ge L\) 即可。而对于 \([l, r]\) 子串中的字符种类,我们可以开四个变量记录当前子串 \([l, r]\) 的字符种类,在 \(l, r\) 两个指针移动使更改这些变量的值即可。
我们这样做的时间复杂度为 \(O(n)\)。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
const int maxn = 1e5 + 10;
int n, L, R, cnt[4];
char s[maxn];
inline int type(char c)
{
if (isupper(c))
return 0;
if (islower(c))
return 1;
if (isdigit(c))
return 2;
return 3;
}
inline bool check()
{
return (cnt[0] > 0) + (cnt[1] > 0) + (cnt[2] > 0) + (cnt[3] > 0) > 2;
}
int main()
{
scanf("%d%d%d%s", &n, &L, &R, s);
int l = 0;
ll ans = 0;
for (int i = 0; i < L; ++i)
++cnt[type(s[i])];
for (int r = L - 1; r < n; ++r)
{
while (r - l + 1 >= L && check())
{
--cnt[type(s[l])];
++l;
}
ans += max(0, l - max(0, r - R + 1));
++cnt[type(s[r + 1])];
}
printf("%lld\n", ans);
return 0;
}
J
本题题意可以概括为统计函数 \(f(x) = \begin{cases} f(x - P) & x \in [P, +\infty) \\ x & x \in (-P, P) \\ f(x + P) & x \in (-\infty, -P] \end{cases}\) 的图像在区域 \(\{(x, y) | L \le x \le R, l \le y \le r\}\) 有多少整点。
不难发现 \(f(x)\) 为奇函数,并且其图像没有经过第二、四象限。所以我们可以考虑将题目中给定的区域按照所在象限分解成四个区域的并。我们对各个象限分别求答案,然后在求和即可。
对于第二、四象限答案为 0,所以我们只需考虑第一、三象限。函数为奇函数关于原点对称,所以第三象限的区域可以对应到第一象限来求解。
但是对于 \(x\) 轴上的点需要额外注意,比如当出现 \(l = 0, L < 0\) 这种情况时,我们仍然需要统计 \(x\) 负半轴上可能的合法整点。所以这里将 \(x\) 轴上的整点和各象限内的整点分开计算。
为了简化代码,我们将 \(L \le 0, R \le 0\) 的情况对称成 \(-R \le x \le -L\) 进行统计,注意此时 \(l, r\) 也需要对称翻转。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
int p, l, r, L, R;
inline int calc(int x, int y)
{
if (1ll * x * y < 0)
return 0;
x = abs(x), y = abs(y);
return x / p * y + min(x % p, y);
}
inline int calc(int y)
{
if (y == 0)
return 0;
y = y < 0 ? max(y, 1 - p) : min(y, p - 1);
if (y > 0)
return calc(R, y) - (L > 0 ? calc(L - 1, y) : 0);
return calc(L, y);
}
int main()
{
scanf("%d%d%d%d%d", &p, &l, &r, &L, &R);
if (L <= 0 && R <= 0)
{
swap(L, R), swap(l, r);
L = -L, R = -R;
l = -l, r = -r;
}
p = abs(p);
int ans = 0;
if (l == 0)
ans = calc(r);
else if (r == 0)
ans = calc(l);
else if (1ll * l * r < 0)
ans = calc(l) + calc(r);
else
{
printf("%d\n", l > 0 ? calc(r) - calc(l - 1) : calc(l) - calc(r + 1));
return 0;
}
if (L == 0)
ans += R / p + 1;
else if (1ll * L * R < 0)
ans += R / p - L / p + 1;
else
ans += R / p - (L - 1) / p;
printf("%d\n", ans);
return 0;
}
K
考虑到 \(P < 0\) 时比较麻烦,故考虑将其转化为 \(P > 0\) 的情况,而根据题目对取模结果符合的规定,\(P\) 取相反数后 \(l, r\) 也要相应的取相反数。
然后考虑将条件改为 \(P \bmod x = P - \lfloor \frac{P}{x} \rfloor x = Q \in [l, r]\),即 \(\dfrac{P - r}{\lfloor \frac{P}{x} \rfloor} \le x \le \dfrac{P - l}{\lfloor \frac{P}{x} \rfloor}\)。所以我们可以考虑除法分块,每个块中 \(\lfloor \frac{P}{x} \rfloor\) 都相等,对于每个块直接统计有多少 \(x\) 满足上文的条件即可。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
int p, l, r, L, R;
inline int calc(int n)
{
n = abs(n);
int res = 0;
for (int i = 1, j, t; i <= n; i = j + 1)
{
t = p / i;
if (t == 0)
{
if (l <= p && p <= r)
res += n - i + 1;
break;
}
j = min(n, p / t);
res += max(0, min(j, (p - l) / t) - max(i, (p - r + t - 1) / t) + 1);
}
return res;
}
int main()
{
scanf("%d%d%d%d%d", &p, &l, &r, &L, &R);
if (p > 0)
{
l = max(l, 0);
if (r < l)
{
puts("0");
return 0;
}
}
else
{
r = min(r, 0);
if (r < l)
{
puts("0");
return 0;
}
swap(l, r);
l = -l, r = -r, p = -p;
}
if (L <= 0 && R <= 0)
{
swap(L, R);
L = -L, R = -R;
}
if (L == 0)
printf("%d\n", calc(R));
else if (1ll * L * R < 0)
printf("%d\n", calc(L) + calc(R));
else
printf("%d\n", calc(R) - calc(L - 1));
return 0;
}
第四场
B
显然对于区间 \([x, y]\) 组成的数字,我们选取的进制只需是该区间内最大的数码加 1 的进制了。所以我们需要维护区间最大值即可。
而对于 \(i\) 位置上的数,我们需要开 9 个树状数组,分别维护 2 到 10 进制下,\(s_i \times k^{n - i + 1}\) 的值。取区间 \([x, y]\) 的和时,只需将树状数组中的答案乘个 \((k^{n - y + 1})^{-1}\) 即可。
Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
const int p = 1000000007;
const int maxn = 1e5 + 10;
int n, q, base[15][maxn], a[15][maxn];
char s[maxn];
inline int power(int a, int b)
{
int res = 1;
while (b)
{
if (b & 1)
res = 1ll * res * a % p;
a = 1ll * a * a % p;
b >>= 1;
}
return res;
}
class BIT
{
private:
int v[maxn], bound;
inline int lowbit(int x)
{
return x & -x;
}
public:
inline void build(int *a, int n)
{
bound = n;
for (int i = 1, j; i <= n; ++i)
{
v[i] = (v[i] + a[i]) % p;
j = i + lowbit(i);
if (j <= n)
v[j] = (v[j] + v[i]) % p;
}
}
inline void add(int pos, int x)
{
for (int i = pos; i <= bound; i += lowbit(i))
v[i] = (v[i] + x) % p;
}
inline int query(int pos)
{
int res = 0;
for (int i = pos; i > 0; i -= lowbit(i))
res = (res + v[i]) % p;
return res;
}
inline int query(int l, int r)
{
return query(r) - query(l - 1);
}
}tree[11];
class Segtree
{
#define lson (u << 1)
#define rson (u << 1 | 1)
private:
int v[maxn << 2], bound;
void build(int l, int r, int u)
{
if (l == r)
{
v[u] = s[l] - '0';
return;
}
int mid = (l + r) >> 1;
build(l, mid, lson), build(mid + 1, r, rson);
v[u] = max(v[lson], v[rson]);
}
void modify(int l, int r, int u, int pos, int k)
{
if (l == r)
{
v[u] = k;
return;
}
int mid = (l + r) >> 1;
if (pos <= mid)
modify(l, mid, lson, pos, k);
else
modify(mid + 1, r, rson, pos, k);
v[u] = max(v[lson], v[rson]);
}
int query(int l, int r, int u, int ql, int qr)
{
if (ql <= l && r <= qr)
return v[u];
int mid = (l + r) >> 1, res = 0;
if (ql <= mid)
res = max(res, query(l, mid, lson, ql, qr));
if (qr > mid)
res = max(res, query(mid + 1, r, rson, ql, qr));
return res;
}
public:
inline void build(int n)
{
bound = n;
build(1, n, 1);
}
inline void modify(int pos, int k)
{
modify(1, bound, 1, pos, k);
}
inline int query(int ql, int qr)
{
return query(1, bound, 1, ql, qr);
}
}help;
inline void modify(int x, int y)
{
help.modify(x, y);
for (int i = 2, cur; i <= 10; ++i)
{
cur = 1ll * y * base[i][n - x + 1] % p;
tree[i].add(x, (cur - a[i][x] + p) % p);
a[i][x] = cur;
}
}
inline int query(int l, int r)
{
int k = max(2, help.query(l, r) + 1);
return 1ll * tree[k].query(l, r) * power(base[k][n - r + 1], p - 2) % p;
}
int main()
{
scanf("%d%d%s", &n, &q, s + 1);
help.build(n);
for (int i = 2; i <= 10; ++i)
base[i][0] = 1;
for (int i = 2; i <= 10; ++i)
{
for (int j = 1; j <= n; ++j)
{
base[i][j] = 1ll * base[i][j - 1] * i % p;
a[i][n - j + 1] = 1ll * (s[n - j + 1] - '0') * base[i][j] % p;
}
tree[i].build(a[i], n);
}
int opt, x, y;
while (q--)
{
scanf("%d%d%d", &opt, &x, &y);
if (opt == 1)
modify(x, y);
else
printf("%d\n", query(x, y));
}
return 0;
}
D
题目简述就是求一个定点到若干条线段的最短距离。对于每条线段,我们可以知道垂线段最短。然而由于是线段,我们不确定该定点到线段是否可以做垂线段,这时我们可以通过线段端点与定点构成的向量和线段的方向向量做点积判断夹角是否为钝角即可。如果存在一个钝角,则不存在垂线段,这是就对两端点分别到定点的最小值取 min 即可。
注意本题的坑点在于坐标可能会达到 \(2 \times 10^{14}\),在这个范围内如果进行乘法运算可能会爆 c++ 中的 long long,所以这里直接使用了 python。
Code
import math
def dis(x1, y1, x2, y2):
return math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
def dot(x1, y1, x2, y2):
return x1 * x2 + y1 * y2
n = int(input())
x0, y0, x, y = list(map(int, input().split()))
ans = dis(x0, y0, x, y)
for i in range(n):
dx, dy = list(map(int, input().split()))
nx = x0 + dx
ny = y0 + dy
if dot(dx, dy, x - x0, y - y0) >= 0 and dot(dx, dy, nx - x, ny - y) >= 0:
A = y0 - ny
B = nx - x0
C = x0 * ny - nx * y0
ans = min(ans, abs(A * x + B * y + C) / math.sqrt(A * A + B * B))
else:
ans = min(ans, dis(x, y, x0, y0), dis(x, y, nx, ny))
x0 = nx
y0 = ny
print(ans)
G
本题显然要考虑统计贡献,由于本题两个子数组不同是要看子数组中的数在原数组当中的位置,所以可以打乱原数组顺序。而本题和最大最小值有关,故考虑对 \(a_i\) 从小到大排序。
排序后,考虑 \(a_i\) 对答案的贡献。显然对于比 \(i\) 小的 \(j\),以 \(a_j\) 为最小值,\(a_i\) 为最大值的子序列个数有 \(2^{i-j}\) 个,所以 \(a_i\) 作为子序列中的最大值对答案的贡献为 \(a_i^{2^0 + 2^1 + \cdots + 2^{i - 2}} = a_i^{2^{i - 1} - 1}\)。同理,\(a_i\) 作为子序列中的最小值对答案的贡献为 \(a_i^{2^{n - i} - 1}\)。再算上只包含 \(a_i\) 本身的子序列,可以得出 \(a_i\) 对答案的总贡献为 \(a_i^{2^{i - 1}} \times a_i^{2^{n - i}}\)。
发现指数位置有次幂的形式,故需要使用拓展欧拉定理求解。
Code
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
using std::min;
using std::max;
using std::sort;
using std::swap;
using std::vector;
using std::pair;
typedef long long ll;
const int p = 1000000007;
const int maxn = 2e5 + 10;
int n, a[maxn], b[maxn], cnt[maxn];
inline int power(int a, int b, int p)
{
int res = 1;
while (b)
{
if (b & 1)
res = 1ll * res * a % p;
a = 1ll * a * a % p;
b >>= 1;
}
return res;
}
int main()
{
scanf("%d", &n);
int ans = 1;
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; ++i)
{
ans = 1ll * ans * power(a[i], power(2, n - i, p - 1), p) % p;
ans = 1ll * ans * power(a[i], power(2, i - 1, p - 1), p) % p;
}
printf("%d\n", ans);
return 0;
}