2022哈理工校赛 题解

比赛链接

L题 NP-hard

给定十进制下的正整数 \(n\),问其在 \(x,y\) 进制下哪个 1 比较多?

\(1\leq n \leq 10^9,2\leq x, y\leq 10\)

#include<bits/stdc++.h>
using namespace std;
int f(int n, int x) {
    int res = 0;
    while (n) {
        res += n % x == 1;
        n /= x;
    }
    return res;
}
char solve()
{
    int n, x, y;
    cin >> n >> x >> y;
    int a = f(n, x), b = f(n, y);
    if (a == b) return '=';
    else if (a > b) return '>';
    else return '<';
}

int main()
{
    int T;
    cin >> T;
    while (T--) cout << solve() << endl;
    return 0;
}

I题 又AK了

计算出每题所需要的时间,然后先做花时间小的在做花时间大的,数学证明如下:

\[sum=a_1+(a_1+a_2)+\cdots+(a_1+a_2+\cdots+a_n) \\ =na_1+(n-1)a_2+\cdots+a_n \]

#include<bits/stdc++.h>
using namespace std;
const int N = 100;
int n, a[N], b[N];
int solve()
{
    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    sort(a + 1, a + n + 1);
    for (int i = 1; i <= n; ++i)
        b[i] = a[i] - a[i - 1];
    sort(b + 1, b + n + 1);
    for (int i = 1; i <= n; ++i)
        b[i] += b[i - 1];
    int res = 0;
    for (int i = 1; i <= n; ++i) res += b[i];
    return res;
}

int main()
{
    int T;
    cin >> T;
    while (T--) cout << solve() << endl;
    return 0;
}

G题 gk的数字游戏

我们有一个游戏:

  1. 首先给定两个正整数 \(n,m\)
  2. 执行操作\(\begin{cases}n=n-m&n\geq m\\m=m-n&n<m\end{cases}\)
  3. 反复执行操作2,直到某个数变为0

问总操作步数。

\(0\leq n,m\leq 10^9\)

这题不难想到辗转相除法及其优化:我们用取模代替减法,然后通过除法来记录操作次数,反复模拟即可。

#include<bits/stdc++.h>
using namespace std;
int solve()
{
    int n, m;
    cin >> n >> m;
    int res = 0;
    while (n > 0 && m > 0) {
        if (n <= m) swap(n, m);
        res += n / m;
        n %= m;
    }
    return res;
}
int main()
{
    int T;
    cin >> T;
    while (T--) cout << solve() << endl;
    return 0;
}

E题 gk的字符串

给定一个仅包含小写字母和问号的字符串,我们现在要把每个问号都替换成小写字母,且要求不存在两个相邻且相同的字符。

\(|s|\leq 10^6\)

贪心的替换即可,a b c 轮着试。

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
char s[N];
void solve()
{
    scanf("%s", s + 1);
    int n = strlen(s + 1);
    s[0] = s[n + 1] = '\0';
    for (int i = 1; i <= n; ++i) {
        if (s[i] != '?') continue;
        for (char c = 'a'; c <= 'z'; ++c)
            if (s[i - 1] != c && s[i + 1] != c) {
                s[i] = c;
                break;
            }
    }
}

int main()
{
    int T;
    cin >> T;
    while (T--) {
        solve();
        puts(s + 1);
    }
    return 0;
}

J题 大数乘法

给定 \(x,y,p\),求出 \(x^y\bmod p\) 的值。

\(0\leq x \leq 10^5,0\leq y\leq 10^{100000},10^5\leq p\leq 10^9+7\)

方法一:欧拉降幂

#include <bits/stdc++.h>
using namespace std;
#define LL long long
LL phi(LL n) {
    LL ans = n;
    for (LL i = 2; i * i <= n; ++i)
        if (n % i == 0) {
            ans = ans / i * (i - 1);
            while (n % i == 0) n /= i;
        }
    if (n > 1) ans = ans / n * (n - 1);
    return ans;
}
const int N = 1000010;
char str[N];
LL read(LL mod, bool &flag) {
    int n = strlen(str + 1);
    LL ans = 0;
    for (int i = 1; i <= n; ++i) {
        ans = ans * 10 + str[i] - '0';
        if (ans >= mod) flag = true;
        ans %= mod;
    }
    return ans;
}
LL power(LL a, LL b, LL mod) {
    LL res = 1;
    while (b) {
        if (b & 1) res = res * a % mod;
        b >>= 1;
        a = a * a % mod;
    }
    return res;
}
LL solve() {
    LL a, b, m;
    scanf("%lld%s%lld", &a, str + 1, &m);
    LL P = phi(m);
    bool flag = false;
    b = read(P, flag);
    if (flag) b += P;
    return power(a, b, m);
}
int main()
{
    int T;
    cin >> T;
    while (T--) cout << solve() << endl;
    return 0;
}

方法二:预处理(来自官方题解)

\(y=\sum\limits_{i=0}^n a_i*10^i\),然后便有

\[x^y=\prod_{i=0}^nx^{a_i*10^i}=\prod_{i=0}^n(x^{10^i})^{a_i} \]

那么我们预处理好 \(x^{10^i}\) 即可。

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 100010;
LL x, p, px[N];
char y[N];
LL calc(LL a, LL b) {
    LL res = 1;
    while (b--) res = res * a % p;
    return res;
}
LL solve() {
    scanf("%lld%s%lld", &x, y, &p);
    int n = strlen(y) - 1;
    px[0] = x;
    for (int i = 1; i <= n; ++i)
        px[i] = calc(px[i - 1], 10);
    LL res = 1;
    for (int i = 0; i <= n; ++i)
        res = res * calc(px[i], y[n - i] - '0') % p;
    return res;
}
int main()
{
    int T;
    cin >> T;
    while (T--) cout << solve() << endl;
    return 0;
}

(涨见识了,高位次的幂原来还可以这么求)

B题 抓球

\(n\) 个黑球,\(m\) 个白球中任选 \(k\) 个球,问连续 \(q\) 次都是 \(k\) 个黑球的概率。

\(1\leq n,m\leq 10^5,1\leq k\leq n+m,1\leq q \leq 10^{12}\),结果对 \(10^9+7\) 取模。

显然,答案为

\[ans=(\frac{C_n^k}{C_{n+m}^k})^q \]

那么掌握线性求组合数和分数取模即可。

#include<bits/stdc++.h>
using namespace std;
//逆元法快速求组合数
#define LL long long
const LL mod = 1e9 + 7;
const int N = 1000010;
LL fact[N << 1], inv[N << 1];
//快速幂
LL power(LL a, LL b) {
    LL res = 1;
    while (b) {
        if (b & 1) res = res * a % mod;
        b >>= 1;
        a = a * a % mod;
    }
    return res;
}
LL getinv(LL x) {
    return power(x, mod - 2) % mod;
}
void init(int n) {
    fact[0] = 1;
    for (int i = 1; i <= n; ++i)
        fact[i] = fact[i - 1] * i % mod;
    inv[n] = power(fact[n], mod - 2);
    for (int i = n - 1; i >= 0; --i)
        inv[i] = inv[i + 1] * (i + 1) % mod;
}
LL C(LL n, LL m) {
    if (m > n) return 0;
    return fact[n] * inv[m] % mod * inv[n - m] % mod;
}
int main()
{
    init(500000);
    int T;
    cin >> T;
    while (T--) {
        LL n, m, k, q;
        cin >> n >> m >> k >> q;
        cout << power(C(n, k) * getinv(C(n + m, k)) % mod, q) << endl;
    }
    return 0;
}

F题 gk的树

给定一个 \(n\) 点的树,问至少需要删多少边,才可以使得每个点的度数都不超过 \(k\)

\(1\leq n,k\leq 10^5\)

方法一:树形DP

对于一个点来说,如果我们要删边,那么无非:

  1. 删除其向子节点所连的边
  2. 删除其向父节点所连的边(根节点除外)

那么,我们记 \(dp_{x,0/1}\) 分别为删除/不删除 \(x\) 节点向父亲的所连边,且使得整个子树都符合度数小于等于 \(k\) 所需删除的最少边数。不太显然(我也不会证明),\(dp_{x,0}\leq dp_{x,1}\)

我们考虑怎么进行计算:

  1. 先初始化:\(dp_{x,0}=0,dp_{x,1}=1\)
  2. 首先递归计算所有子树
  3. 我们假设该节点不需要删点,那么把所有子树的 \(dp_0\) 值加上去
  4. 考虑删边,我们想一下怎么删最好:对于统计答案而言,删边等同于用对应连向子树的 \(dp_1\) 来替换 \(dp_0\) 值;那么,我们直接对于所有子树,按照 \(dp_1-dp_0\) 来排序,然后贪心选择最小的

最后答案为 \(dp_{root,0}\),总复杂度 \(O(n\log n)\)

#include<bits/stdc++.h>
using namespace std;
const int N = 100010, M = 200010;
int n, k;
vector<int> G[N];
int dp[N][2];
void dfs(int x, int fa) {
    dp[x][0] = 0, dp[x][1] = 1;
    vector<int> vec;
    for (int y : G[x])
        if (y != fa) {
            dfs(y, x);
            vec.push_back(y);
        }
    sort(vec.begin(), vec.end(), [](int a, int b) {
        return dp[a][1] - dp[a][0] < dp[b][1] - dp[b][0];
    });
    //dpx0
    for (int y : vec) dp[x][0] += dp[y][0], dp[x][1] += dp[y][0];
    int cnt = G[x].size() - k;
    for (int i = 0; i < cnt; ++i) {
        int y = vec[i];
        dp[x][0] += dp[y][1] - dp[y][0];
    }
    //dpx1
    for (int i = 0; i < cnt - 1; ++i) {
        int y = vec[i];
        dp[x][1] += dp[y][1] - dp[y][0];
    }
}
int solve() {
    cin >> n >> k;
    for (int i = 1; i <= n; ++i) G[i].clear();
    for (int i = 1; i < n; ++i) {
        int u, v;
        cin >> u >> v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs(1, 0);
    return dp[1][0];
}
int main()
{
    int T;
    cin >> T;
    while (T--) cout << solve() << endl;
    return 0;
}

方法二:贪心

事实上,我们发现 \(dp_{x,0}\leq dp_{x,1}\leq dp_{x,0}+1\),这意味着我们其实可以将上面的树形 DP 中排序的那一部分复杂度降下来,使得总复杂度降到 \(O(n)\)。(本质上其实就是一种小贪心了属于是)

方法三:网络流

这方法真的是让我小刀喇屁股——开了大眼了。

因为树必然是一个二分图,所以我们对树进行黑白染色,然后从源点 S 向黑点连容量为 k 的边,黑点向白点连容量为 1 的边,白点向汇点 T 连容量为 k 的边,然后用 Dinic 跑一次最大流,所得结果即为可以保留的边的最大数量。(通过建模,我们巧妙将度数的限制转化为了流量的限制:每个点都至多流入/流出 k 的流量。

Dinic 跑网络流的上限复杂度为 \(O(n^2m)\),但是一般来说都跑不到上限,可以解决 \(10^4\)\(10^5\) 大小的图。特别的,对于这类二分图跑网络流,复杂度是 \(O(m\sqrt{n})\) 的。

C题 迷宫

给定一整个 \(n*m*h\) 的三维方格空间,外加一个空的点集 \(S\)(初始状态下 \(S\) 为空)。现在,我们有 \(q\) 次操作,分两类:

  1. 向点集中塞入一个点 \(x\)
  2. 给定一个点 \(x\),问这个点到点集内某个点的最短路径(即 \(\min\limits_{y\in S}\operatorname{dis}(x,y)\))(两点之间的曼哈顿距离)

\(1\leq q,nmh\leq 10^5\)

我们考虑下面两个问题(新学的):

  1. 给定若干个点构成的点集,怎么计算空间中所有点到这个点集的最短路径?

    直接将这些点全部推入一个队列,并将 \(dis_x\) 设置为 0,然后开始 BFS 一遍即可。(可以想象成从一个虚点向他们连边,然后从这个点开始 BFS 即可)

  2. 对于上面的问题,如果我们需要动态加入新点,怎么办?

    实际上,我们在原来 dis 数组上操作,将新点的 dis 值设为 0,然后再次 BFS 即可。(最短路中的动态加点问题)

处理好了这个问题,我们发现本题拥有类似其他题(我说不上来,但是有印象)的性质:

  1. 当点集数量小于某个数的时候,不如直接将待查询点和点集里面的点一一计算并比较
  2. 如果点集数量有点大,那么我们不如直接 BFS 一遍,然后每次查询都可以 \(O(1)\) 查询(如果后面不需要插入点的话)

对于上面那个点集,如果需要插入点的话,我们不妨先用一个 vec 备着,在数量比较小的时候先暴力,等到一定数量了再插进去 BFS。

好了,那么这个数,我们应该设置成多少呢?

熟悉分块的话,不难想到,记 \(N=10^5\),那么这个数就记为 \(\sqrt{N}\)

  1. 插入新点进入集合,等到集合达到 \(\sqrt{N}\) 时就将其全部推入队列来重构,均摊复杂度 \(O(\sqrt{N})\)
  2. 对于查询,在之前的点直接 dis \(O(1)\) 查询,集合里面直接 \(O(\sqrt{N})\) 来查询,均摊同样复杂度

总复杂度 \(O(N\sqrt{N})\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
const int LIM = 1000;
int n, m, h, Q;

struct P { int x, y, z; };
vector<P> vec;

inline int getid(P A) {
    return h * (A.x * m + A.y) + A.z;
}
const int dx[6] = {-1, 1, 0, 0, 0, 0}, dy[6] = {0, 0, 1, -1, 0, 0}, dz[6] = {0, 0, 0, 0, 1, -1};
bool in(int x, int y, int z) {
    return x >= 1 && x <= n && y >= 1 && y <= m && z >= 1 && z <= h;
}
int dis[N];
queue<P> q;
void BFS() {
    for (P x : vec) {
        q.push(x);
        dis[getid(x)] = 0;
    }
    vec.clear();
    while (!q.empty()) {
        P point = q.front(); q.pop();
        int x = point.x, y = point.y, z = point.z;
        int pid = getid(point);
        for (int i = 0; i < 6; ++i) {
            int nx = x + dx[i], ny = y + dy[i], nz = z + dz[i];
            int nid = getid({nx, ny, nz});
            if (in(nx, ny, nz) && dis[nid] > dis[pid] + 1) {
                q.push({nx, ny, nz});
                dis[nid] = dis[pid] + 1;
            }
        }
    }
    return;
}
int getdis(P A, P B) {
    return abs(A.x - B.x) + abs(A.y - B.y) + abs(A.z - B.z);
}
void solve() {
    cin >> n >> m >> h >> Q;
    memset(dis, 0x3f, sizeof(dis));
    vec.clear();
    while (Q--) {
        int opt, x, y, z;
        scanf("%d%d%d%d", &opt, &x, &y, &z);
        if (opt == 1) {
            vec.push_back({x, y, z});
            if ((int)vec.size() >= LIM) BFS();
        }
        else {
            int id = getid({x, y, z}), ans = dis[id];
            for (P point : vec)
                ans = min(ans, getdis(point, {x, y, z}));
            cout << ans << endl;
        }
    }
}
int main()
{
    int T;
    cin >> T;
    while (T--) solve();
    return 0;
}
posted @ 2022-04-04 19:46  cyhforlight  阅读(44)  评论(0编辑  收藏  举报