2021杭电多校第一场 (1) 个人补题记录
比赛链接:Here
1001 - Mod, Or and Everything
签到,
打表发现与 2的次方相关联
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int _; for (cin >> _; _--;) {
ll n; cin >> n;
if (n == 1)cout << "0\n";
else {
int k = log(n) / log(2);
ll sum = pow(2, k);
if (sum != n)cout << sum - 1 << "\n";
else cout << sum / 2 - 1 << "\n";
}
}
}
1002 - Rocket land
待补
1003 - Puzzle loop
待补
1004 - Another thief in a Shop
待补
1005 - Minimum spanning tree
这里借用一些官方解法说明:
对于编号为3~n的点,将所有编号为合数的点向其约数连边,编号为质数的点向2连边,不难证明这样形
成的生成树是最小的。
总的边权和为(质数的和 *2+合数的和),用欧拉筛预处理前缀和即可。
效率: \(\mathcal{O}(n)\)
const int N = 1e7 + 10;
int cnt, vis[N], prime[N];
void init() {
for (int i = 2; i < N; i ++ ) {
if (!vis[i]) prime[cnt ++ ] = i;
for (int j = 0; prime[j] * i <= N; j ++ ) {
vis[i * prime[j]] = true;
if (i % prime[j] == 0) break;
}
}
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
init();
int _; for (cin >> _; _--;) {
int n; cin >> n;
ll cnt = 0;
for (int i = 3 ; i <= n; ++i) {
if (!vis[i])cnt += 2 * i;
else cnt += i;
}
cout << cnt << "\n";
}
}
1006 - Xor sum (Good ,字典树)
题意很简单:
给定长度为 \(n (\le1e5)\) 的序列,找到最短的连续子序列使得 异或和
不小于 \(k(a_i,k\le2^{30})\) ,
如果存在多个满足条件的序列,请输出左端点最小的连续子序列的左端点和右端点,
如果不存在连续子序列的异或和
不小于 \(k\) 则输出 -1
思路参考于 聆竹听风
看到题目第一想法是暴力解决,但注意到 \(n\) 的范围是 \(n\le1e5\) ,写BF肯定TLE了,
由于异或满足以下性质:\(sum[l...r] = sum[1...l - 1]⊕sum[1...r]\)
所以我们可以用前缀和,
设 \(sum[1...i] = S_i\) ,特别地,定义 \(S_0 = 0\),于是有:
从而可以将原问题转化为,在 \(S_0,S_1,...,S_n\) 中寻找一对在序列中最短的 \(S_i,S_j(i < j)\) ,满足 \(S_i ⊕ S_j \ge k\),最终答案即为 \([i + 1,j]\) ( \(i+1\) 是左端点,\(j\) 是右端点)
正如最开始说的一样,最暴力的想法就是利用两重循环,第一层循环从 \([1,n]\) 枚举 \(d\) ,第二层枚举 \(i(0\le i \le n + 1 - d)\) ,判断 \(S_i ⊕ S_{i + d - 1} \ge k\) 是否成立,成立则说明答案已经找到,跳出循环,不成立则继续循环。
// 暴力写法
int l = -1, r = n;
for (int i = 1; i <= n; ++i) {
int x = 0;
for (int j = i; j < n; ++j) {
x ^= a[j];
if (j - i >= r - l)break;
if (x >= k)l = i, r = j;
}
}
if (l >= 0)cout << l << " " << r << "\n";
else cout << "-1\n";
因为暴力做法时间复杂度为 \(\mathcal{O}(n^2)\),会超时,所以需要优化。不妨考虑枚举右侧端点 \(i\) ,尝试是否能在区间 \([0,i - 1]\) 上快速找到离 \(i\) 最近的 \(j\) 使得 \(S_i ⊕ S_j \ge k\) 。
既然是异或问题,一定和二进制相关,而题目给出的范围是 \(0\le a_i\le 2^{30}\) ,所以 \(S_i\) 也在这个范围中,说明 \(S_i\) 可以用 \(30\) 位二进制表示,于是 \(S_i\) 可以看成长度为 \(30\) 的 "01
"字符串。
故而可以考虑在枚举 \(i\) 的时候,动态地维护一个包含 \(S_0,S_1,...,S_{i−1}\) 的"01
"字典树,其中深度小的节点存储高位,深度大的节点存储低位。字典树的每个节点附加存储着这个节点所表示的前缀(从高位开始的"01"串)最后一次在数列 \(S_0,S_1,...,S_{i−1}\) 中出现的位置,没出现过位置就记成 \(−1\)。
然后让 \(S_i\) 和字典树中的串进行带剪枝的逐位异或:
为了方便描述,记 \(S_i\) 中从高位到低位数起第 \(j\) 位(以下简称“第 \(j\) 位”)为 \(S_{ij}\),\(k\) 中第 \(j\) 位是 \(k_j\)。
假设目前正在考虑第 \(j\) 位的情况,即到达了字典树的第 \(j−1\) 层(根节点为空前缀,把它当成第 \(0\) 层),考虑往哪个方向上的子树深入下去(并不是两个方向上都需要深入,即剪枝)。
- 当 \(k_j=1\) ,就要求字典树中所表示串的第 \(j\) 位和 \(S_{ij}\) 异或的结果也是 \(1\) ,才有可能使得最终异或结果大于等于 \(k\),由于\(S_{ij}⊕1\) 与 \(S_{ij}\) 异或结果是 \(1\),故此时需要往 \(S_{ij}⊕1\) 方向的那个子树上深入。
- 当\(k_j=0\) ,说明字典树中所表示串的第 \(j\) 位和 \(S_{ij}\)异或的结果可以是 \(0\),也可以是 \(1\)
- 若字典树中所表示串的第 \(j\) 位和 \(S_{ij}\) 异或的结果是 \(1\) ,由于\(S_{ij}⊕1\) 与 \(S_{ij}\) 异或结果是 \(1\),即考虑往 \(S_{ij}⊕1\) 方向,发现此时无需进行后续位的异或,也可知道最终异或结果大于\(k\),故无需往 \(S_{ij}⊕1\) 方向的子树下深入,直接利用往 \(S_{ij}⊕1\) 方向的节点所附加的信息更新答案(别忘了,每个节点附加记录了这个节点所代表前缀最后一次在数列 \(S_0,S_1,...,S_{i−1}\) 中出现的位置,这是一种剪枝,也是优化的关键)。
- 若字典树中所表示串的 第 \(j\) 位和 \(S_{ij}\) 异或 的结果是 \(0\) ,由于 \(S_{ij}\) 与 \(1\) 异或结果是 \(0\) ,即考虑往\(S_{ij}\)方向,此时还需要进行后续位的异或,故需要往 \(S_{ij}\) 方向的子树上深入。
假如逐位异或能够进行到最后一位,那说明异或到最后才比较出大于等于 \(k\),此时直接利用叶节点附加信息更新答案。
在 \(S_i\) 与字典树中的串进行带剪枝的逐位异或之后,就需要把 \(S_i\) 这个串插入到字典树中,注意插入过程需要更新节点的附加信息,以便后续计算。
时间复杂度分析:由于字典树只会往一个方向遍历,设整数序列最大的数为 \(P\) (最大为 \(2^{30}\)),则树的最大深度是\(logP\),整数序列长度为 \(n\),故复杂度为 \(O(nlogP)\),本题中可以认为是 \(O(30n)\)。
【AC Code】
const int N = 1e5 + 10, M = 3e6 + 10;
int a[N];
int ch[M][2], val[M];
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int _; for (cin >> _; _--;) {
int n, k, tot = 1;
cin >> n >> k;
a[0] = 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
a[i] ^= a[i - 1];
}
ch[1][0] = ch[1][1] = 0;
val[1] = 1;
int l = -1, r = n + 1;
for (int i = 0; i <= n; ++i) {
int now = 1, ans = -1;
for (int j = 29; j >= 0; --j) {
int dig = (a[i] >> j) & 1;
if ((k >> j) & 1) // k的当前位为1,只能和dig异或结果为1,才可能大于等于k
now = ch[now][dig ^ 1]; // 与dig异或结果为1的数是dig^1
else { // k的当前位为0,和dig异或结果可以是1也可以是0
if (ch[now][dig ^ 1]) // 和dig异或结果为1,后面的位都无须看,结果一定大于k
ans = max(ans, val[ch[now][dig ^ 1]]);
// 和dig异或结果是1的情况就无须遍历,只需要遍历和dig异或结果为0的情况
now = ch[now][dig];
}
// 节点没了
if (now == 0) break;
}
if (now) ans = max(ans, val[now]);
// 更新当前最小区间序列
if (ans >= 0 and i - ans < r - l) {
l = ans, r = i;
}
now = 1;
for (int j = 29; j >= 0; --j) {
int dig = (a[i] >> j) & 1;
if (!ch[now][dig]) {
ch[now][dig] = ++tot;
ch[tot][0] = ch[tot][1] = 0;
val[tot] = -1;
}
now = ch[now][dig];
val[now] = max(val[now], i);
}
}
if (l == -1 and r == n + 1) cout << "-1\n";
else cout << l + 1 << " " << r << "\n";
}
}
顺便贴一下官方解释:
对数列做前缀异或,将题面转化为找两个距离最近的数,使得他们的异或值不小于 \(k\)。
枚举靠右的那个数,同时维护字母树,字母树每个节点保存范围内最靠右的点的位置。根据k来询
问对应的 \(log\) 个节点,从而更新答案。
效率: \(O(nlogn)\)
1007 - Pass!
待补
1008 - Maximal submatrix (Good)
题意:
给定一个数字矩阵,求出最大面积的满足每列非递减的矩阵
一开始dp写的,但发现TLE了,然后转念想到这完全可以用单调栈解决。在思考的同时队友WJX大牛已经AC了,所以比赛的时候就没往下想。赛后补一下
本题正解为原矩阵转为01矩阵,再使用悬线法求最大01矩阵即可
01 矩阵:1 代表该位置是否比前一位小
悬线法算法讲解:Here
复杂度 \(O(n^2)\)
const int N = 2e3 + 10;
// a为原矩阵,b转为01矩阵
int a[N][N], b[N][N];
int H[N], Q[N];
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int _; for (cin >> _; _--;) {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j) {
cin >> a[i][j];
b[i][j] = 0; // init
if (i > 1)b[i][j] = (a[i][j] >= a[i - 1][j]);
}
for (int i = 1; i <= m; ++i) H[i] = 0;
int ans = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (b[i][j] == 0) H[j] = 1;
else H[j]++;
}
int cnt = 0;
H[m + 1] = 0;
for (int j = 1; j <= m + 1; ++j) {
while (cnt and H[Q[cnt]] > H[j]) {
ans = max(ans, (j - Q[cnt - 1] - 1) * H[Q[cnt]]);
--cnt;
}
Q[++cnt] = j;
}
}
cout << ans << '\n';
}
}
在贴一下 WJX 大牛的单调栈写法:
const int N = 2010;
int c[N][N];
int n, m, h[N][N];
LL work(int h[N], int n) {
int l[N], r[N], q[N];
h[0] = h[n + 1] = -1;
int tt = 0;
q[0] = 0;
for (int i = 1; i <= n; i++) {
while (h[i] <= h[q[tt]]) tt--;
l[i] = q[tt];
q[++tt] = i;
}
tt = 0;
q[0] = n + 1;
for (int i = n; i >= 1; i--) {
while (h[i] <= h[q[tt]]) tt--;
r[i] = q[tt];
q[++tt] = i;
}
LL ans = 0;
for (int i = 1; i <= n; i++) {
ans = max(ans, (LL)h[i] * (r[i] - l[i] - 1));
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
int t;
cin >> t;
while (t--) {
memset(c, 0, sizeof(c));
memset(h, 0, sizeof(h));
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> c[i][j];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (c[i][j] >= c[i - 1][j]) h[i][j] = h[i - 1][j] + 1;
else h[i][j] = 1;
LL ans = 0;
for (int i = 1; i <= n; i++) {
ans = max(ans, work(h[i], m));
}
cout << ans << endl;
}
return 0;
}
1009 -KD-Graph (Good)
思维+并查集,签到(雾
题意都能看懂就不放了
将边按权值从小到大排序,每一阶段取出同权值的所有边,将这些边的端点用并查集两两合并,若某一阶段的全部边合并完,并查集数量为k,则当前阶段合并边的权值就是答案,否则输出-1。
const int N = 1e6 + 10;
struct node {int u, v, w;} q[N];
int f[N];
int find(int x) {return f[x] == x ? x : f[x] = find(f[x]);}
bool cmp(node a, node b) {return a.w < b.w;}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int _; for (cin >> _; _--;) {
int n, m, k;
cin >> n >> m >> k;
int now = n, ans = 0;
for (int i = 1; i <= n; i++) f[i] = i;
for (int i = 1; i <= m; i++) cin >> q[i].u >> q[i].v >> q[i].w;
sort(q + 1, q + 1 + n, cmp);
for (int i = 1 ; i <= m; ++i) {
if (q[i].w != q[i - 1].w) {if (now == k) break;}
if (find(q[i].u) == find(q[i].v))continue;
now --, f[find(q[i].u)] = find(q[i].v), ans = q[i].w;
}
cout << (now == k ? ans : - 1) << "\n";
}
}
1010 - zoto
待补
1011 - Necklace of Beads
看完题目感觉能做,但证明写了半个小时硬是没想出来,淦,太菜了
这里贴一下正解证明过程
【AC Code】
using ll = long long;
const int N = 1000001, mod = 998244353;
ll n, k, ans, fac[N], inv[N], invfac[N], f[N], p[N], vis[N], prime[N], cnt, phi[N];
void init() {
phi[1] = p[0] = fac[0] = invfac[0] = inv[1] = fac[1] = invfac[1] = 1;
p[1] = 2;
for (ll i = 2; i < N; i++) {
if (!vis[i])prime[++cnt] = i, phi[i] = i - 1;
for (ll j = 1; j <= cnt && i * prime[j] < N; j++) {
vis[i * prime[j]] = 1;
if (i % prime[j] == 0) {
phi[i * prime[j]] = phi[i] * prime[j];
break;
}
phi[i * prime[j]] = phi[i] * (prime[j] - 1);
}
p[i] = p[i - 1] * 2ll % mod;
fac[i] = fac[i - 1] * i % mod;
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
invfac[i] = invfac[i - 1] * inv[i] % mod;
}
}
ll C(ll n, ll m) {
if (n < 0 || m < 0 || m > n)return 0;
return fac[n] * invfac[m] % mod * invfac[n - m] % mod;
}
ll get(ll n, ll k) {
f[0] = n & 1 ? 0 : 2;
for (ll m = 1; m <= n; m++)
f[m] = (p[m] * (C(n - m, m) + C(n - m - 1, m - 1)) + f[m - 1]) % mod;
return f[min(n, k)];
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
init();
int _; for (cin >> _; _--;) {
ans = 0;
cin >> n >> k;
for (ll d = n; d >= 1; d--)
if (n % d == 0)
ans = (ans + phi[n / d] * get(d, k * d / n)) % mod;
cout << ans *inv[n] % mod << "\n";
}
}