2022武汉大学新生赛 题解

比赛链接

H题 wwhhuu

给定数字 \(n\),要求你创造一个仅包含 \(w,h,u\) 三个字符的长度为 \(n\) 的字符串,且其中为 \(\text{whu}\) 的子序列(不一定连续)尽可能多,要求输出子序列至多可以有多少。

\(1\leq n\leq 500\)

想要使得子序列尽可能多,显然应该使得字符串形似 \(\text{wwwhhhuuuu}\) 这种类型。那么问题就转变为了一个数学问题:

\(a+b+c=n\) 的约束条件下,使得 \(abc\) 最大。(\(a,b,c\) 均为正整数)

换元+不等式+求导发现,当 \(a=b=c=\frac{n}{3}\) 的时候答案最大。(如果有剩的余数,平均加上去即可)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
int main()
{
    LL n, a[3];
    cin >> n;
    a[0] = a[1] = a[2] = n / 3;
    for (int i = 0; i < n % 3; ++i) ++a[i];
    cout << a[0] * a[1] * a[2] << endl;
    return 0;
}

A题 仓鼠快速签到

首先借助互联网发现了最后两题答案均为 C,然后排除掉第八题选 A 的可能(-1 次,队员让羊吃了属于是)。

随后,我们直接写一个暴力 DFS,枚举出所有选项,然后 check 一下,最后得到答案并输出。

#include<bits/stdc++.h>
using namespace std;
char str[20];
bool isPrime(int x) {
    return x == 2 || x == 3 || x == 5 || x == 7;
}
int calc() {
    int dp[20];
    memset(dp, 0, sizeof(dp));
    dp[1] = 1;
    for (int i = 2; i <= 10; ++i)
        if (str[i] == str[i - 1]) dp[i] = dp[i - 1] + 1;
        else dp[i] = 1;
    int Max = -1;
    for (int i = 1; i <= 10; ++i) Max = max(Max, dp[i]);
    return Max;
}
bool can() {
    int a = 0, b = 0, c = 0;
    for (int i = 1; i <= 10; ++i)
        if (str[i] == 'A') ++a;
        else if (str[i] == 'B') ++b;
        else ++c;
    //t1
    int cnt1 = 0;
    if (isPrime(a)) ++cnt1;
    if (isPrime(b)) ++cnt1;
    if (isPrime(c)) ++cnt1;
    if (cnt1 != str[1] - 'A' + 1) return false;
    //t2
    if (b != str[2] - 'A' + 2) return false;
    //t3
    if (abs(a - c) != 'D' - str[3]) return false;
    //t4
    if (abs(str[4] - str[3]) != 'C' - str[4]) return false;
    //t5
    if (str[5] == 'A') {
        if (str[2] != str[3]) return false;
    }
    else if (str[5] == 'B') {
        if (str[4] != str[5]) return false;
    }
    else {
        if (str[6] != str[7]) return false;
    }
    //t6
    if (str[6] == 'A') {
        if (a != 2) return false;
    }
    else if (str[6] == 'B') {
        if (b != 3) return false;
    }
    else {
        if (c != 4) return false;
    }
    //t7
    if (calc() != str[7] - 'A' + 2) return false;
    //t8
    if (str[8] == 'A') return false;
    //true
    return true;
}
void dfs(int dep) {
    if (dep == 9) {
        if (can()) puts(str + 1);
        return;
    }
    for (char c = 'A'; c <= 'C'; ++c) {
        str[dep] = c;
        dfs(dep + 1);
    }
}
int main()
{
    str[9] = str[10] = 'C', str[11] = '\0';
    dfs(1);
}
print("CBABBACCCC")

D题 和谐之树

显然,深度深的点,其编号显然更大,所以我们有显然策略:每次都向深度深的那个子树遍历,直到到达叶子节点时返回其编号。

我们观察发现,若一个树所管辖区间的长度为 \(n\),当 \(2^d<n\leq 2^{d+1}\) 时,这个树的深度为 \(d+2\)

考虑到每次比较的时候,两个子树的大小至多相差一,所以我们只考虑小的那个子树的大小是否是 2 的幂即可(如果小的那个子树的大小为 \(2^t\),大的那个是 \(2^t+1\),那么大的子树的深度便高了 1)。

(附:我们仅在子树大小不等的时候判断即可,相同的时候就不必了,我被这个卡了一发)

总计 \(T\) 组询问,每次询问的复杂度都为 \(O(\log n)\) 左右,总复杂度 \(O(T\log n)\)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
bool check(LL x) {
    return (x & (x - 1)) == 0;
}
LL solve(LL n, LL root) {
    if (n == 1) return root;
    LL mid = (n + 1) >> 1;
    LL l = mid, r = n - mid;
    if (l == r || !check(r)) return solve(r, root << 1 | 1);
    else return solve(l, root << 1);
}
int main()
{
    int T;
    cin >> T;
    while (T--) {
        LL n;
        cin >> n;
        cout << solve(n, 1) << endl;
    }
    return 0;
}

J题 传闻档案

图论经典建模技巧:如果要统计其他若干个点到特定点的信息,那么我们反向建图,然后从这个特定点开始跑。

我们直接来建立反图,建好之后按照权值排序,先从权值最大的开始 BFS,将遍历到的点全部打上标记(并统计答案),随后再从权值次大的开始 BFS(之前遍历过的点不再遍历,因为他们显然能够在原图中到达更大的权值点),以此类推,直到所有点都被打上了标记为止,随后输出答案均可。

本题排序的复杂度为 \(O(n\log n)\),BFS 的复杂度为 \(O(n+m)\)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 100010;
int n, m;
vector<int> G[N];
LL a[N], vis[N];
void BFS(int sp, LL val) {
    queue<int> q;
    q.push(sp); vis[sp] = val;
    while (!q.empty()) {
        int x = q.front(); q.pop();
        for (int y : G[x])
            if (!vis[y]) {
                q.push(y); vis[y] = val;
            }
    }
}
int main()
{
    //read & build
    priority_queue<pair<LL, int> > q;
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        q.push(make_pair(a[i], i));
    }
    for (int i = 1; i <= m; ++i) {
        int u, v;
        cin >> u >> v;
        G[v].push_back(u);
    }
    //run
    while (!q.empty()) {
        auto p = q.top(); q.pop();
        if (!vis[p.second]) BFS(p.second, p.first);
    }
    //output
    LL ans = 0;
    for (int i = 1; i <= n; ++i) ans += vis[i];
    cout << ans << endl;
    return 0;
}

G题 寄寄子的生日

给定一个长度为 \(n-1\) 的排列,数值从 2 到 n。

对于两个数 \(a,b\),如果 \(\gcd(a,b)=1\),那么这两个数可以相互交换位置。问:我们能否经过不超过 \(2n\) 次交换,使得这个排列从小到大排序?

\(n\leq 10^3\),输出任意方案均可(不要求交换次数最小)

我们先考虑将 \(n\) 换到最高位:

  1. \(n\) 恰好在最高位,跳过,处理前面 \([2,n-1]\) 的子问题

  2. \(n\) 与最高位的那个数互质,那么交换即可,随后处理 \([2,n-1]\) 的子问题

  3. \(n\) 与最高位的那个数不互质:

    我们能否从 \([2,n]\) 之间找到一个数,使得它与 \(n\) 和最高位的数都互质?如果存在,怎么找?

    后者还好,直接 \(O(n)\) 扫一遍,但是时候一定存在呢?

    这个东西我不好说,但是隐约感觉确实存在(例如现场中的最大的质数或者啥的)

\(n\) 放到了最高位,然后我们处理 \(n-1\),依次处理,直到每个数都放到了期望位置。我们发现,每次处理某个数字的时候,至多进行 2 次操作,所以总交换次数必然小于 \(2n\),而总时间复杂度也在 \(O(n^2\log n)\) 这样(求 gcd 的复杂度在 \(O(\log n)\) 左右),也是可以接受的。

#include<bits/stdc++.h>
using namespace std;
int gcd(int a, int b) {
    if (!b) return a;
    return gcd(b, a % b);
}
const int N = 1010;
int n, a[N], pos[N];
vector<pair<int, int> > vec;
void myswap(int x, int y) {
    pos[a[x]] = y, pos[a[y]] = x;
    swap(a[x], a[y]);
    vec.push_back(make_pair(x - 1, y - 1));
}
int main()
{
    //read
    cin >> n;
    for (int i = 2; i <= n; ++i)
        cin >> a[i], pos[a[i]] = i;
    //solve
    for (int i = n; i >= 2; i--) {
        if (a[i] == i) continue;
        int p_i = pos[i];
        if (gcd(i, a[i]) == 1) myswap(i, p_i);
        else {
            int p_j = -1;
            for (int j = 2; j < i; ++j)
                if (gcd(i, a[j]) == 1 && gcd(a[i], a[j]) == 1) {
                    p_j = j; break;
                }
            myswap(p_j, i);
            myswap(p_i, i);
        }
    }
    //
    cout << vec.size() << endl;
    for (auto p : vec) cout << p.first << " " << p.second << endl;
    return 0;
}

F题 仓鼠与炸弹

我们不难写出正反两次 hash,这样就可以 \(O(1)\) 判断两个长度为 \(m\) 的串是否对称了。

不过,暴力枚举每个串的起点的话,复杂度最坏 \(O(n^2)\),我们需要对其进行优化。

我们考虑原先的暴力结构:

for (int i = 1; i <= n; ++i)
    for (int j = i + m; j + m - 1 <= n; ++j)
        if (check(i, j)) ++ans;

换言之,我们需要在区间 \([i+m,n-m+1]\) 中判断有多少个 j 满足 \(check(i,j)\) 为真。

为了保险,我们采取了双哈希的策略,每个点都做两次哈希,所以我们干脆用 pair 存一下。

pair<LL, LL> hash_p1[N], hash_p2[N];
for (int i = 1; i <= n - m + 1; ++i) {
    hash_p1[i] = make_pair(hash1.query(i, i + m - 1), hash2.query(i, i + m - 1));
    hash_p2[i] = make_pair(hash3.query(i, i + m - 1), hash4.query(i, i + m - 1));
}

那么每次 check 就可以简化成这样:

bool check(int i, int j) { return hash_p1[i] == hash_p2[j]; }

那么问题就变成了:

从左到右枚举 \(i\),然后每次查询区间 \([i+m,n-m+1]\) 中有多少个 \(j\),满足 \(hash\_p2[j]\) 等于 \(hash\_p1[i]\)

注意到这个区间,右端点不变,左端点每次向右移动 1,这意味着我们能否使用一些数据结构来将其优化,例如平衡树?(统计集合内有多少个数等于 \(x\),删除一个数 \(x\)

其实我们可以直接用一个 map 维护就好了,总复杂度 \(O(n\log n)\)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 100010;
struct Hash_STL_Z {
    LL P, mod, mul[N], hashcode[N];
    void buildHash(LL _P, LL _mod, int n, char *str) {
        P = _P, mod = _mod;
        mul[0] = 1, hashcode[0] = 0;
        for (int i = 1; i <= n; ++i)
            mul[i] = mul[i - 1] * P % mod;
        for (int i = 1; i <= n; ++i)
            hashcode[i] = (hashcode[i - 1] * P + str[i]) % mod;
    }
    LL query(int l, int r) {
        return (hashcode[r] - hashcode[l - 1] * mul[r - l + 1] % mod + mod) % mod;
    }
} hash1, hash2;
struct Hash_STL_F {
    LL P, mod, mul[N], hashcode[N];
    void buildHash(LL _P, LL _mod, int n, char *str) {
        P = _P, mod = _mod;
        mul[0] = 1, hashcode[n + 1] = 0;
        for (int i = 1; i <= n; ++i)
            mul[i] = mul[i - 1] * P % mod;
        for (int i = n; i >= 1; --i)
            hashcode[i] = (hashcode[i + 1] * P + str[i]) % mod;
    }
    LL query(int l, int r) {
        return (hashcode[l] - hashcode[r + 1] * mul[r - l + 1] % mod + mod) % mod;
    }
} hash3, hash4;
int n, m;
char str[N];
bool check(int i, int j) {
    return hash1.query(i, i + m - 1) == hash3.query(j, j + m - 1)
        && hash2.query(i, i + m - 1) == hash4.query(j, j + m - 1);
}
pair<LL, LL> hash_p1[N], hash_p2[N];
map<pair<LL, LL> , LL> vis;
int main()
{
    //read & build
    scanf("%d%d%s", &n, &m, str + 1);
    hash1.buildHash(2102, 20021014, n, str);
    hash2.buildHash(2103, 20021023, n, str);
    hash3.buildHash(2102, 20021014, n, str);
    hash4.buildHash(2103, 20021023, n, str);
    //solve
    for (int i = 1; i <= n - m + 1; ++i) {
        hash_p1[i] = make_pair(hash1.query(i, i + m - 1), hash2.query(i, i + m - 1));
        hash_p2[i] = make_pair(hash3.query(i, i + m - 1), hash4.query(i, i + m - 1));
    }
    for (int i = m + 1; i <= n - m + 1; ++i)
        vis[hash_p2[i]]++;
    LL ans = 0;
    for (int i = 1; i <= n - 2 * m + 1; ++i) {
        ans += vis[hash_p1[i]];
        vis[hash_p2[i + m]]--;
    }
    cout << ans << endl;
    return 0;
}
posted @ 2022-04-05 22:01  cyhforlight  阅读(34)  评论(0编辑  收藏  举报