本文发布于博客园,会跟随补题进度实时更新,若您在其他平台阅读到此文,请前往博客园获取更好的阅读体验。
跳转链接:https://www.cnblogs.com/TianTianChaoFangDe/p/19007314
开题情况
7.22牛客多校3 : 5题 - ADEFJ
7.24牛客多校4 : 1题 - F
7.21杭电多校2 : 4题 - 2、6、8、9
7.25杭电多校3 : 4题 - 2、7、8、12
暑期多校第二周,这周打得有点不尽如人意,主要是好几场的mid题就开始卡了,尤其是牛客多校4,被自己一个很低级的错误葬送了B题,还需多练!
本人个人补题情况
7.22牛客多校3 : 3题 - BEH
7.24牛客多校4 : 2题 - BG
7.21杭电多校2 : 3题 - 7、9、12
7.25杭电多校3 : 2题 - 4、9
有些题场上虽然过了,但一看并非最优解,就也纳入补题阵列了。
牛客多校3补题
B - Bitwise Puzzle
赛时试图枚举所有初始状态来求解,但还是一WA再WA。
首先 \(b\) 可以进行按位右移操作,所以 \(b\) 是可以变成 \(0\) 的,那么,我们就有一个大致的方向了:用 \(b\) 来改 \(a\),把 \(a\) 改成 \(c\),然后把 \(b\) 异或上 \(a\)。
当 \(a\) 和 \(b\) 不都为 \(0\) 的时候,如果 \(a\) 和 \(b\) 的最高位不同,我们一定可以通过一次 \(a \oplus b\),来把 \(a\) 和 \(b\) 的最高位变成一样的(因为最高位不等的话,在该位,一定有一个 \(1\) 一个 \(0\),异或一下就可以把 \(0\) 改成 \(1\)),记这个最高位为 \(k\)。
由于我们要把 \(b\) 变成 \(0\),所以 \(b\) 的最高位 \(1\) 会逐步右移,就足以修改掉 \(a\) 的 \(\leq k\) 的位了,因此对于 \(a\) 的 \(\leq k\) 的位,只需要通过把 \(b\) 逐步右移,对于 \(b\) 当前的最高位,比对 \(a\) 和 \(c\) 在该位是否相等,如果不相等,就用一次异或操作进行修改。由于 \(b\) 的最高位往左全都是 \(0\),所以修改不会对 \(a\) 已修改的位造成影响。
那如果 \(c\) 的最高位 \(> k\) 呢?\(b\) 右移逐步修改是修改不到更高的位的,那么我们可以换个角度,既然 \(b\) 不能左移,那就让 \(a\) 左移,我们用第 \(k\) 位去修改 \(a\) 的第 \(k\) 位,然后通过左移操作把修改的位往左运输过去,不就行了吗。
因此,我们的操作顺序应该是:
- 如果 \(a\) 和 \(b\) 最高位不等,那么用一次 \(a \ oplus b\) 把最高位对齐。
- 通过 \(a \oplus b\) 和 \(a << 1\) 把 \(> k\) 的位修改得和 \(c\) 一样。
- 通过 \(a \oplus b\) 和 \(b >> 1\) 把 \(\leq k\) 的位修改得和 \(c\) 一样。
- 用 \(a \oplus b\) 把 \(b\) 改成 \(c\)。
计算一下操作次数:第一步最多执行 \(1\) 次操作,第二步和第三步每次修改最多有 \(2\) 个操作参与,总的位数最多为 \(31\) 位,所以最多 \(31 * 2 = 62\) 次,最后一步执行 \(1\) 次操作,加起来刚好 \(64\) 次,非常稳定!
再看一下上面这个做法成立的条件,我们需要把 \(a\) 和 \(b\) 的最高位对齐,因此,至少要有一个非 \(0\),如果两个都是 \(0\),则没有任何的修改机会,当且仅当 \(c = 0\) 的时候有解,否则无解。
点击查看代码
/* by 01022.hk - online tools website : 01022.hk/zh/calcvolume.html */
#include <bits/stdc++.h>
#define int long long
using i64 = long long;
void solve() {
int a, b, c;std::cin >> a >> b >> c;
if(a == b && b == 0) {
if(c == 0) {
std::cout << 0 << "\n\n";
} else {
std::cout << -1 << '\n';
}
return;
}
std::vector<int> op;
if(std::__lg(a) != std::__lg(b)) {
if(std::__lg(a) < std::__lg(b)) {
a ^= b;
op.push_back(3);
} else {
b ^= a;
op.push_back(4);
}
}
int ch = std::__lg(c) - std::__lg(a);
int now = std::__lg(a);
if(ch > 0) {
for(int i = 0;std::__lg(a) < std::__lg(c);i ++) {
if((c >> (std::__lg(c) - i) & 1) != (a >> now & 1)) {
a ^= b;
op.push_back(3);
}
a <<= 1;
op.push_back(1);
}
}
for(int i = now;i >= 0;i --) {
if((c >> i & 1) != (a >> i & 1)) {
a ^= b;
op.push_back(3);
}
b >>= 1;
op.push_back(2);
}
while(b) {
b >>= 1;
op.push_back(2);
}
b ^= a;
op.push_back(4);
std::cout << op.size() << '\n';
for(auto &x : op) {
std::cout << x << ' ';
}
std::cout << '\n';
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int t = 1;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
E - Equal
这个题赛时队友想的。
首先,如果 \(n\) 为奇数,那么一定可行,因为对除了某个数字外的其它的所有数字(一定是偶数个)都做一个相同的操作,等于对这个数字做一个单点的反向操作,那么一定可以把所有数字都变成一样的。(类似于上半年南昌邀请赛的一个题)
如果 \(n\) 为 \(2\),特殊判断一下,当且仅当两个数字相同的时候才可行,因为此时只能对这两个数字操作,且操作都相同。
如果 \(n\) 为其它偶数,那么,只需要判断,各个质数在这些所有数中的出现次数,是否为偶数次,如果为偶数次,则一定可行,因为每次操作对质数次数变化都是偶数次,如果一开始次数为奇数次,那么最后必然会有孤立的数,并且不能像奇数的情况那样,通过修改其它的来看作反向修改自己,因为此时除了孤立出来的那个数,剩下的数的总数是偶数个。
处理方法,赛时选择对每个数进行唯一分解来计数,通过素数剪枝卡过去了,此做法很极限,瓶颈在于唯一分解。
那么再观察一下这个题要我们干嘛,质数出现次数为偶数,那什么方法很适合用来统计信息出现的奇偶性呢?
那不就是异或哈希嘛!
我们通过给每个质数随机一个哈希值,然后在素筛的过程中通过异或处理出每个数的哈希值,由于次数为偶数的时候,异或值为 \(0\),因此,判断数组中所有分解出来的质数的出现次数是否都为偶数,也就是判断数组所有数的哈希值异或起来的值是否为 \(0\)。
时间复杂度:\(O(nlog(logn))\)
点击查看代码
/* by 01022.hk - online tools website : 01022.hk/zh/calcvolume.html */
#include <bits/stdc++.h>
#define int long long
const int N = 5e6 + 9;
bool notprime[N];
std::vector<int> prime;
bool vis[N];
int hash[N];
std::mt19937_64 rnd(std::chrono::steady_clock::now().time_since_epoch().count());
int ran(int l, int r)
{
return rnd() % (r - l + 1) + l;
}
void init() {
notprime[0] = notprime[1] = true;
for(int i = 2;i < N;i ++) {
if(!notprime[i])prime.push_back(i);
for(int j = 0;j < prime.size() && prime[j] * i < N;j ++) {
notprime[prime[j] * i] = true;
if(i % prime[j] == 0) {
break;
}
}
}
for(int i = 0;i < prime.size();i ++) {
hash[prime[i]] = ran(1e9, 1e18);
for(int j = prime[i] * 2;j < N;j += prime[i]) {
int tmp = j;
while(tmp % prime[i] == 0) {
tmp /= prime[i];
hash[j] ^= hash[prime[i]];
}
}
}
}
void solve() {
int n;std::cin >> n;
std::vector<int> a(n);
for(auto &x : a) {
std::cin >> x;
}
if(n & 1) {
std::cout << "YES\n";
return;
}
if(n == 2) {
if(a[0] == a[1]) {
std::cout << "YES\n";
} else {
std::cout << "NO\n";
}
return;
}
int ans = 0;
for(auto &x : a) {
ans ^= hash[x];
}
std::cout << ((ans == 0) ? "YES\n" : "NO\n");
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
init();
int t = 1;std::cin >> t;
while(t --) {
solve();
}
return 0;
}
H - Head out to the Target
赛时磕 B 去了,没看这题,现在回想起来应该和队友一起磕这题的。
这个题首先要明确一点,如果我们假设一个结点 \(x\) 是最后答案所在点,那么必定是从根沿着路径走下来,不会走到别的地方去(也就是把别的路切了),不然一定不优。
那么其实我们需要关注的,就是到某个时间点的时候,目标到根的路径上的所有结点,也就是目标的所有祖先结点,是否都有可能在最优情况下走过。
当一个目标出现时,只需要把它的最近的祖先结点往下扩展一格就行了,一个祖先结点是可能走过的,那么这个目标出现时,完全可以认为当前棋子就在这个祖先结点上,因为在其它结点的话,勾引过来到的位置,也一定是之前已经到过的,所以每次最多扩展一格。
只需要判断目标在每个结点时,该结点是否有可能走过即可(因为也可以切断它下面的边避免它被勾引走)。
这个祖先结点可以使用倍增进行查找,单次时间复杂度为 \(O(logn)\),由于每个点最多被扩展一次,所以最多扩展 \(n\) 次,然后就一定有解了。
时间复杂度:\(O(nlogn)\)。
点击查看代码
#include <bits/stdc++.h>
#define int long long
const int N = 1e7 + 9;
void returm() {
int n, k;std::cin >> n >> k;
std::vector<std::array<int, 30>> fa(n + 1, std::array<int, 30>{});
for(int i = 2;i <= n;i ++) {
std::cin >> fa[i][0];
}
for(int k = 1;k < 30;k ++) {
for(int i = 1;i <= n;i ++) {
fa[i][k] = fa[fa[i][k - 1]][k - 1];
}
}
std::vector<int> vis(n + 1);
vis[1] = true;
vis[0] = true;
auto get = [&](int st) -> int {
for(int k = 29;k >= 0;k --) {
if(vis[fa[st][k]])continue;
st = fa[st][k];
}
return st;
};
int ans = -1;
while(k --) {
int u, l, r;std::cin >> u >> l >> r;
if(ans != -1)continue;
if(vis[u]) {
ans = l;
continue;
}
for(int i = l;i <= r;i ++) {
int now = get(u);
vis[now] = true;
if(now == u) {
ans = i;
break;
}
}
}
std::cout << ans << '\n';
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int t = 1;//std::cin >> t;
while(t --) {
returm();
}
}
牛客多校4补题
B - BlindAlley
赛时战犯的一题,由于最大值自底向上转移的时候循环写反了,导致赛时没能开出,记录于此,警钟长鸣!
还是写写思路吧,这个题,其实关注点总结下来就一句话:一个点,是否有可能在某一时刻,以为能向右走过去走到终点,于是向右走到了这个点,但这个点实际上是无法抵达终点的,同时也没有回头路了。
翻译一下题意如下:
- 在某个点以为能向右走过去,翻译一下其实就是:当前列为 \(j\),向右走了过后,该右点能向右到达的最远点的列数 \(\geq j + k\)。
- 这个点实际上是无法抵达终点,翻译一下,其实就是:从重点出发按题目规则反向走走不到这个点。
因此,思路就很简单了。
首先从起点跑一遍BFS再配合一次从右往左的简单DP转移最值,预处理一下每个点可以到达的最远列。
然后从重点跑一遍BFS,找出所有可以抵达的点。
然后再从起点跑一遍BFS,不过这次和第一次不一样,这次在走点的时候,要判断一下是否满足上述第1条题意翻译,然后对于可以抵达的点,判断一下它是不是可以抵达终点的点,如果不是,那就直接输出 Yes
即可。
点击查看代码
#include <bits/stdc++.h>
#define int long long
const int N = 1e7 + 9;
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, 1, -1};
void returm() {
int n, m, k;std::cin >> n >> m >> k;
std::vector a(n, std::vector<char>(m));
for(auto &x : a) {
for(auto &c : x) {
std::cin >> c;
}
}
std::vector mx(n, std::vector<int>(m));
std::queue<std::pair<int, int>> q;
q.push({0, 0});
std::vector vis(n, std::vector<int>(m));
auto inmp = [&](int x, int y) -> bool {
return x >= 0 && x < n && y >= 0 && y < m;
};
mx[0][0] = 0;
while(q.size()) {
auto [x, y] = q.front();
q.pop();
for(int i = 0;i < 3;i ++) {
int nx = x + dx[i];
int ny = y + dy[i];
if(!inmp(nx, ny))continue;
if(a[nx][ny] == '1')continue;
if(vis[nx][ny])continue;
mx[nx][ny] = std::max(y, ny);
vis[nx][ny] = true;
q.push({nx, ny});
}
}
for(int j = m - 1;j >= 0;j --) {
if(j + 1 < m) {
for(int i = 0;i < n;i ++) {
if(a[i][j + 1] == '0')mx[i][j] = std::max(mx[i][j], mx[i][j + 1]);
}
}
for(int i = 0;i < n;i ++) {
if(a[i][j] == '1')continue;
if(i - 1 >= 0 && a[i - 1][j] == '0')mx[i][j] = std::max(mx[i][j], mx[i - 1][j]);
}
for(int i = n - 1;i >= 0;i --) {
if(a[i][j] == '1')continue;
if(i + 1 < n && a[i + 1][j] == '0')mx[i][j] = std::max(mx[i][j], mx[i + 1][j]);
}
}
std::vector v(n, std::vector<int>(m));
v[0][m - 1] = true;
q.push({0, m - 1});
while(q.size()) {
auto [x, y] = q.front();
q.pop();
for(int i = 0;i < 4;i ++) {
if(i == 2)continue;
int nx = x + dx[i];
int ny = y + dy[i];
if(!inmp(nx, ny))continue;
if(a[nx][ny] == '1')continue;
if(v[nx][ny])continue;
v[nx][ny] = true;
q.push({nx, ny});
}
}
std::vector u(n, std::vector<int>(m));
q.push({0, 0});
u[0][0] = true;
while(q.size()) {
auto [x, y] = q.front();
q.pop();
if(!v[x][y]) {
std::cout << "Yes\n";
return;
}
for(int i = 0;i < 3;i ++) {
int nx = x + dx[i];
int ny = y + dy[i];
if(!inmp(nx, ny))continue;
if(a[nx][ny] == '1')continue;
if(u[nx][ny])continue;
if(mx[nx][ny] < y + k)continue;
u[nx][ny] = true;
q.push({nx, ny});
}
}
std::cout << "No\n";
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int t = 1;std::cin >> t;
while(t --) {
returm();
}
}
G - Ghost intheParentheses
看到括号对,作为一个栈匹配的经典模型,常见的思路就是转化为前缀问题,对 (
赋值为 \(1\),对 )
赋值为 \(-1\)。
题目说对于未知的括号对,要让它们的填法是唯一的,来满足合法的括号序列。
那考虑一下,什么样的括号对,如果未知,其填法不是唯一的呢?
对于一个合法的括号对序列,应该满足它的任何一个前缀和都 \(\geq 0\),如果交换一个左括号和一个右括号,该条件仍然满足,那么就说明不唯一!
因此,唯一的条件是,交换任何一个左括号和任何一个右括号,都会导致存在一个前缀值 \(< 0\)。
回到这个题目,我们对于每一个点 \(i\),找到右边最近的一个满足前缀值 \(= 1\) 的位置 \(j\),这时候, \(i\) 左侧的左括号,都可以未知,且只能是左括号未知,\(j\) 右侧的右括号,都可以未知,且只能是右括号未知,因为此时交换左边未知左括号和右边未知的右括号,必然导致 \(j\) 处的前缀值变成 \(-1 < 0\)。
同时,考虑 \(i\) 到 \(j\) 之间的字符都已知,因为 \(i\) 到 \(j\) 之间的字符如果未知,都会对 \(j\) 的位置造成影响,不便于计数。
由于选出的未知字符一定是左边很多左括号,右边很多右括号,不会出现先右括号再左括号的情况(出现这种情况了则可以交换他俩了,前缀值只会增大),这样的话,为了不重不漏,我们可以以每个左括号为分界点来进行计数,一定可以做到不重不漏。
但其实还是会漏掉一种情况,由于在上面的枚举过程中,我们保证了每种情况至少有一个左括号是不选为未知的,所以所有左括号都被选为未知的情况会被漏掉,并且这时候一定唯一,所以最后再加上这种情况即可。
点击查看代码
#include <bits/stdc++.h>
#define int long long
const int M = 998244353;
int power(int n, int m) {
int res = 1;
while(m) {
if(m & 1)res = res * n % M;
n = n * n % M;
m >>= 1;
}
return res;
}
int inv(int x) {
return power(x, M - 2) % M;
}
void returm() {
std::string s;std::cin >> s;
int n = s.size();
s = ' ' + s;
std::vector<int> pre(n + 2), suf(n + 2), sum(n + 2);
for(int i = 1;i <= n;i ++) {
sum[i] = sum[i - 1] + (s[i] == '(' ? 1 : -1);
}
for(int i = 1;i <= n;i ++) {
pre[i] = pre[i - 1] + (s[i] == '(');
}
for(int i = n;i >= 1;i --) {
suf[i] = suf[i + 1] + (s[i] == ')');
}
std::vector<int> p(n + 1);
p[0] = 1;
for(int i = 1;i <= n;i ++) {
p[i] = p[i - 1] * 2 % M;
}
int ans = 0;
for(int i = 1, j = 1;i <= n;i ++) {
j = std::max(j, i);
while(j <= n && sum[j] > 1)j ++;
if(s[i] == '(') {
ans = (ans + p[pre[i - 1]] * p[suf[j + 1]] % M) % M;
}
}
ans = (ans + p[n / 2]) % M;
ans = ans * inv(p[n]) % M;
std::cout << (ans % M + M) % M << '\n';
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int t = 1;//std::cin >> t;
while(t --) {
returm();
}
}
杭电多校2补题
杭电多校3补题
作者: 天天超方的
出处: https://www.cnblogs.com/TianTianChaoFangDe
关于作者:ACMer,算法竞赛爱好者
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显醒目位置给出, 原文链接 如有问题, 可邮件(1005333612@qq.com)咨询.