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\),于是有:

\[sum[l...r] = S_{l - 1}⊕S_r \]

从而可以将原问题转化为,在 \(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\) 层),考虑往哪个方向上的子树深入下去(并不是两个方向上都需要深入,即剪枝)。

  1. \(k_j=1\) ,就要求字典树中所表示串的\(j\)\(S_{ij}\) 异或的结果也是 \(1\) ,才有可能使得最终异或结果大于等于 \(k\),由于\(S_{ij}⊕1\)\(S_{ij}\) 异或结果是 \(1\),故此时需要往 \(S_{ij}⊕1\) 方向的那个子树上深入。
  2. \(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}\)),则树的最大深度是\(log⁡P\),整数序列长度为 \(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

看完题目感觉能做,但证明写了半个小时硬是没想出来,淦,太菜了

这里贴一下正解证明过程

1011-证明1

1011-证明2

【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";
    }
}
posted @ 2021-07-21 16:44  RioTian  阅读(400)  评论(2编辑  收藏  举报