C
贪心
a数组长度n,b数组长度m,每次取一对a,b可以拼成一个成品。要求b至少是a的两倍。问最多能拼出来多少个?
不妨考虑每个b能否找到一个a(如果考虑每个a能否找到b其实是同理的),那么对b来说,a至少要是当前bi的一半大,所以越大的a,越通用,小的a只能给晓得b用。所以要尽可能配对,应该先用小的,如果不用后面可能就用不了了。
对a,b,都升序排序,枚举b,对于每个b,把小于b一半的a都删掉,因为后面的b更大了,如果当前bi都用不了,后面的更用不了。移除后最小的ai与当前bi配对。这样贪心,最后得到的配对就是最大配对。
c
void solve() {
int n, m;
cin >> n >> m;
vi a(n + 1), b(m + 1);
rep(i, 1, n) {
cin >> a[i];
}
rep(i, 1, m) {
cin >> b[i];
}
sort(a.begin() + 1, a.end());
sort(b.begin() + 1, b.end());
queue<int> q;
rep(i, 1, n) {
q.push(a[i]);
}
int ans = 0;
rep(i, 1, m) {
int x = b[i];
while (q.size() && q.front() * 2 < x) {
q.pop();
}
if (q.size()) {
// cout << q.front() << ' ' << x << '\n';
ans++;
q.pop();
}
}
cout << ans << '\n';
}
D
思维 多源BFS
网格图上,每个黑点每秒会把相邻的白点染黑,相邻的定义是八联通。并且每秒原本的黑点会变成白点。问k=1e100秒后每个点的颜色?
不可能模拟,一定是有结论的,一般就是循环。打表或者注意到,可以发现,如果初始只有一个黑点,进入循环后,类似一个波的传播,八联通意义下,距离奇数的位置奇数秒时黑的,偶数秒是白的,距离偶数的位置反过来。如果有多个点相互叠加,那么一个点只要到最近的黑色源点,距离是奇数,那么奇数秒就是黑的,偶数秒是白的。
于是要求的就是初始,每个白点到最近的黑点距离。考虑黑点开始多源BFS即可。
这里有点小bug:一整块黑色,内部的黑色会在第一秒开始后变成白的,并且第二秒不会在编程黑的,和独立的黑色点不一样了,不能作为独立的波源了,可以视为不存在。
最简单的处理方法是,手动模拟第一秒,此时第一秒已经把整块黑色的情况都处理完了,从第一秒的结果开始,再跑上面说的多源BFS
我这里的代码没用这个处理,那么就要麻烦一点,需要从黑色,白色分别开始多源BFS,然后复杂分类讨论
c
void solve() {
int n, m;
cin >> n >> m;
vvi a(n + 1, vi(m + 1));
int cnt = 0;
rep(i, 1, n) {
rep(j, 1, m) {
char c;
cin >> c;
if (c == '#') {
a[i][j] = 1;
cnt++;
}
}
}
if (!cnt || cnt == n * m) {
rep(i, 1, n) {
rep(j, 1, m) {
cout << '.';
}
cout << '\n';
}
return;
}
queue<pii>q;
vvi d(n + 1, vi(m + 1, -1));
rep(i, 1, n) {
rep(j, 1, m) {
if (a[i][j]) {
q.push({i, j});
d[i][j] = 0;
}
}
}
while (q.size()) {
auto [x, y] = q.front();
q.pop();
rep(i, -1, 1) {
rep(j, -1, 1) {
if (!i && !j)continue;
int nx = x + i, ny = y + j;
if (nx < 1 || nx > n || ny < 1 || ny > m)continue;
if (d[nx][ny] == -1) {
d[nx][ny] = d[x][y] + 1;
q.push({nx, ny});
}
}
}
}
queue<pii>q1;
vvi d1(n + 1, vi(m + 1, -1));
rep(i, 1, n) {
rep(j, 1, m) {
if (!a[i][j]) {
q1.push({i, j});
d1[i][j] = 0;
}
}
}
while (q1.size()) {
auto [x, y] = q1.front();
q1.pop();
rep(i, -1, 1) {
rep(j, -1, 1) {
if (!i && !j)continue;
int nx = x + i, ny = y + j;
if (nx < 1 || nx > n || ny < 1 || ny > m)continue;
if (d1[nx][ny] == -1) {
d1[nx][ny] = d1[x][y] + 1;
q1.push({nx, ny});
}
}
}
}
rep(i, 1, n) {
rep(j, 1, m) {
if (a[i][j]) {
if (d1[i][j] % 2) {
cout << '#';
} else {
cout << '.';
}
} else {
if (d[i][j] % 2) {
cout << '.';
} else {
cout << '#';
}
}
}
cout << '\n';
}
}
E
数论 计数
问有多少x,y<=N,满足xy的字符串拼接得到的数字,模m意义下和x+y同余
核心数学转化:首先,我们来看定义 concat(x,y)\text{concat}(x, y)concat(x,y)。设正整数 yyy 在十进制下的位数(长度)为 LLL。那么将 xxx 和 yyy 拼接在一起,在数学上等价于将 xxx 向左移动 LLL 位,再加上 yyy:concat(x,y)=x⋅10L+y\text{concat}(x, y) = x \cdot 10^L + yconcat(x,y)=x⋅10L+y题目要求的同余条件为:concat(x,y)≡x+y(modM)\text{concat}(x, y) \equiv x + y \pmod Mconcat(x,y)≡x+y(modM)将上面的代数式代入:x⋅10L+y≡x+y(modM)x \cdot 10^L + y \equiv x + y \pmod Mx⋅10L+y≡x+y(modM)两边同时减去 yyy:x⋅10L≡x(modM)x \cdot 10^L \equiv x \pmod Mx⋅10L≡x(modM)再移项提公因式,得到最终的判定方程:x⋅(10L−1)≡0(modM)x \cdot (10^L - 1) \equiv 0 \pmod Mx⋅(10L−1)≡0(modM)
关键结论:通过上述变形,我们发现一个至关重要的性质:xxx 是否满足条件,只取决于 yyy 的十进制位数 LLL,而与 yyy 的具体数值无关。
既然只与 LLL 相关,而 y≤Ny \le Ny≤N,那么 LLL 的取值范围是非常有限的(如果 N≤1018N \le 10^{18}N≤1018,max(L)=18\max(L) = 18max(L)=18;如果 N≤109N \le 10^9N≤109,max(L)=9\max(L) = 9max(L)=9)。我们可以直接枚举 yyy 的位数 LLL。
对于一个固定的位数 LLL:
- 计算合法的 yyy 的个数满足位数恰好为 LLL 且不超过 NNN 的正整数 yyy 的区间为 max(1,10L−1),min(N,10L−1)\\max(1, 10\^{L-1}), \\min(N, 10\^L - 1)max(1,10L−1),min(N,10L−1)。如果左端点大于右端点,说明不存在这样的 yyy,直接跳过。否则,合法的 yyy 的数量为:county(L)=min(N,10L−1)−10L−1+1\text{count}_y(L) = \min(N, 10^L - 1) - 10^{L-1} + 1county(L)=min(N,10L−1)−10L−1+1
- 计算合法的 xxx 的个数对于固定的 LLL,我们需要在 1,N1, N1,N 中寻找有多少个 xxx 满足:x⋅(10L−1)≡0(modM)x \cdot (10^L - 1) \equiv 0 \pmod Mx⋅(10L−1)≡0(modM)这等价于:MMM 必须整除 x⋅(10L−1)x \cdot (10^L - 1)x⋅(10L−1)。为了让 xxx 尽可能小,我们可以消除 10L−110^L - 110L−1 和 MMM 的公共部分。令:g=gcd(10L−1,M)g = \gcd(10^L - 1, M)g=gcd(10L−1,M)那么上面的条件等价于 xxx 必须是 Mg\frac{M}{g}gM 的倍数。令 step=Mgstep = \frac{M}{g}step=gM,则 xxx 必须满足:x≡0(modstep)x \equiv 0 \pmod{step}x≡0(modstep)因为 1≤x≤N1 \le x \le N1≤x≤N,所以在这个范围内满足条件的 xxx 的个数非常容易计算,即:countx(L)=⌊Nstep⌋\text{count}_x(L) = \lfloor \frac{N}{step} \rfloorcountx(L)=⌊stepN⌋
- 乘法原理求和对于当前的位数 LLL,xxx 和 yyy 的选择是相互独立的。因此,当前长度带来的合法对数 (x,y)(x, y)(x,y) 为:AnsL=countx(L)×county(L)\text{Ans}_L = \text{count}_x(L) \times \text{count}_y(L)AnsL=countx(L)×county(L)将所有可能的 LLL 对应的结果累加,并在过程中对 998244353998244353998244353 取模,即可得到最终答案。
c
void solve() {
int n, m;
cin >> n >> m;
i128 p = 1;
int ans = 0;
while (p <= n) {
i128 hi = p * 10 - 1;
int g = gcd((int)(hi % m), m);
int c1 = n / (m / g);
i128 mx = hi;
if (hi > n)mx = n;
int c2 = mx - p + 1;
c1 %= M2, c2 %= M2;
ans = (ans + c1 * c2 % M2) % M2;
p *= 10;
}
cout << ans << '\n';
}
F
树的直径 线段树 LCA
树上点初始黑色,每次操作把一个点的颜色反转,然后求树上两个黑色点之间的最大距离。
树上一个点集的最大距离,考虑包含这些点的子树,就是这个子树的直径,树的直径有一个很好的性质,增加一个点z,原本的直径xy,新的直径只可能是xy,xz,yz三者之一。
所以对于这个问题,白色点染黑时,只用考虑新增的黑点,和原有子树内的直径,产生的新的解,取max。问题是黑点变白,相当于删点,不好做。
删除不好做,一般的思路是不删除,改成撤销,或者用线段树合并编程只有增加,没有删除。
第一个思路简单说一下,就是线段树分治的思想,不好删除,那么在dfs返回的时候直接回滚答案。实现起来有点麻烦
第二个思路实现起来更自然,就是我们把每个点都放到线段树的叶子上,每次合并两个区间的点,假设两个区间的直径xy,pq都是确定的,那么合并后整个区间的直径,只可能是xy,pq,xp,xq,yp,yq这几个中的一个,分别计算长度,取max即可。快速计算树上路径长度,可以用LCA配合深度。
注意这里虽然把树放到序列上建线段树了,但是并不需要dfs序,加上也没错,但是不必要。
c
struct Tree {
#define ls u<<1
#define rs u<<1|1
struct Node {
int l, r, x, y, ans;
Node operator+(const Node &o) {
Node res;
res.l = l;
res.r = o.r;
res.ans = -1;
if (ans == -1) {
res.x = o.x;
res.y = o.y;
res.ans = o.ans;
} else if (o.ans == -1) {
res.x = x;
res.y = y;
res.ans = ans;
}
else {
auto cal = [&](int x, int y)->void{
int t = dis(x, y);
if (t > res.ans) {
res.ans = t;
res.x = x, res.y = y;
}
};
cal(x, y);
cal(o.x, o.y);
cal(x, o.x);
cal(x, o.y);
cal(y, o.x);
cal(y, o.y);
}
return res;
}
} tr[N << 2];
void pushup(int u) {
tr[u] = tr[ls] + tr[rs];
}
void build(int u, int l, int r) {
tr[u] = {l, r, l, l, 0};
if (l == r) return;
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
pushup(u);
}
void modify(int u, int idx, int val = -1) {
if (tr[u].l == tr[u].r) {
if (tr[u].ans == -1) {
tr[u].x = tr[u].y = idx;
tr[u].ans = 0;
} else {
tr[u].ans = -1;
}
return;
} else {
int mid = (tr[u].l + tr[u].r) >> 1;
if (mid >= idx) modify(ls, idx, val);
else modify(rs, idx, val);
pushup(u);
}
}
Node query(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r) return tr[u];
int mid = (tr[u].l + tr[u].r) >> 1;
if (r <= mid)return query(ls, l, r);
if (l > mid)return query(rs, l, r);
return query(ls, l, r) + query(rs, l, r);
}
} t;
void solve() {
int n;
cin >> n;
rep(i, 2, n) {
lg[i] = lg[i >> 1] + 1;
}
vvi g(n + 1);
rep(i, 1, n - 1) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
auto &&dfs = [&](auto &&dfs, int u, int fa)->void{
F[0][u] = fa;
rep(i, 1, lg[d[u]]) {
F[i][u] = F[i - 1][F[i - 1][u]];
}
for (int v : g[u]) {
if (v == fa)continue;
d[v] = d[u] + 1;
dfs(dfs, v, u);
}
};
dfs(dfs, 1, 0);
t.build(1, 1, n);
int q;
cin >> q;
rep(i, 1, q) {
int x;
cin >> x;
t.modify(1, x);
cout << t.query(1, 1, n).ans << '\n';
}
}