AtCoder Beginner Contest 243

ABC 怎么这么难。赛后补的,\(\rm G\) 瞅了题解。\(\rm Ex\) 我知道,这题大毒瘤,就没打算写,是最小割和最小割计数,后者跟计算几何有关系。

A - Shampoo

给出 \(v,a,b,c\),轮流给 \(v\) 减去 \(a,b,c\),求当 \(v\) 第一次为负数时,减去的数是哪个。(\(1\le v,a,b,c\le 10^5\))

可以直接模拟。不过也可以取余数找到剩下的然后判断。时间复杂度 \(\mathcal{O}(1)\)

#include <cstdio>
int main()
{
    int v, a, b, c; scanf("%d%d%d%d", &v, &a, &b, &c);
    int res = v % (a + b + c);
    if (res < a) puts("F");
    else 
    {
        res -= a;
        if (res < b) puts("M");
        else puts("T");
    }
    return 0;
}

B - Hit and Blow

给出两个长为 \(n\) 的互不相同序列 \(A,B\),分别求满足 \(i=j,A_i=B_j\)\(i\ne j,A_i=B_j\) 的有序数对 \((i,j)\) 的数量。(\(1\le n\le 10^3,1\le A_i,B_i\le 10^9\))

前者可以直接判,二者的和可以用 std::map 解决。后者作差即可得到。时间复杂度 \(\mathcal{O}(n\log n)\)

#include <map>
#include <cstdio>
const int N = 1e3 + 10; std::map<int, int> mp; int a[N];
int main()
{
    int n, cnt = 0, tot = 0; scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d", a + i), mp[a[i]] = 1;
    for (int i = 1, x; i <= n; ++i)
        scanf("%d", &x), cnt += (x == a[i]), tot += mp[x];
    printf("%d\n%d\n", cnt, tot - cnt); return 0; 
}

C - Collision 2

给出 \(n\) 个二维平面上的人,第 \(i\) 个人位于 \((x_i,y_i)\),坐标互不相同,一开始面朝 \(s_i\in\{\mathtt{L,R}\}\),分别表示左和右。求如果所有人按照相同速度一直走下去会不会相撞。(\(2\le n\le 2\times 10^5,0\le x_i,y_i\le 10^9\))

注意到每种 \(y\) 坐标是独立的。所以考虑记录每个 \(y\) 坐标对应的,\(s_i=\tt R\) 的人,\(x_i\) 的最小值。这样,所有 \(s_i=\tt L\) 的人,如果 \(x_i\) 坐标大于 \(y_i\) 坐标对应的最小值,则就会相撞,反之如果小于或 \(y_i\) 坐标上没有 \(\tt R\) 的人,就不会相撞。上述过程可以用 std::map 轻松实现,时间复杂度 \(\mathcal{O}(n\log n)\)

#include <map>
#include <cstdio>
const int N = 2e5 + 10; int x[N], y[N]; char s[N]; std::map<int, int> mp;
int main()
{
    int n; scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d%d", x + i, y + i);
    scanf("%s", s + 1);
    for (int i = 1; i <= n; ++i)
        if (s[i] == 'R')
        {
            if (!mp.count(y[i])) mp[y[i]] = x[i];
            else mp[y[i]] = std::min(mp[y[i]], x[i]);
        }
    for (int i = 1; i <= n; ++i)
        if (s[i] == 'L')
            if (mp.count(y[i]) && mp[y[i]] < x[i]) return puts("Yes"), 0;
    puts("No"); return 0;
}

D - Moves on Binary Tree

给出一棵结点可看做无穷多的满二叉树,\(i\) 号结点的左右儿子分别为 \(2i,2i+1\)。初始时在结点 \(x\),有长为 \(n\) 的操作序列 \(s\),其中 \(s_i\in\{\tt U,L,R\}\),分别表示向父亲,左儿子,右儿子走。求最终位于哪个结点,保证答案 \(\le 10^{18}\)。(\(1\le n\le 10^6,1\le x\le 10^{18}\))

直接模拟肯定会爆 long long。但注意到题目的限制,所以我们只需要求出最终有用的操作序列,然后模拟就不会爆 long long 了。具体来讲,维护一个栈,如果 \(s_i\in\{\tt L,R\}\),就把它压到栈里。如果 \(s_i=\tt U\),则如果栈为空,就令 \(x\leftarrow \lfloor\frac{x}{2}\rfloor\),否则弹出栈顶。将最终得到的操作序列施加到最终的 \(x\) 上即可,时间复杂度 \(\mathcal{O}(n)\)

#include <cstdio>
const int N = 1e6 + 10; typedef long long ll; char s[N]; int c[N], tp;
int main()
{
    int n; ll x; scanf("%d%lld%s", &n, &x, s + 1);
    for (int i = 1; i <= n; ++i)
        if (s[i] == 'U') { if (!tp) x /= 2; else --tp; }
        else if (s[i] == 'L') c[++tp] = 0;
        else c[++tp] = 1;
    if (tp < 0)
        for (int i = 1; i <= -tp; ++i) x /= 2;
    else 
        for (int i = 1; i <= tp; ++i)
            if (!c[i]) x *= 2; else x = x * 2 + 1;
    printf("%lld\n", x); return 0;
}

E - Edge Deletion

给出一张 \(n\) 个点 \(m\) 条边的简单连通无向图,一条边能被删去,当且仅当:

  • 删去后原图依然连通。
  • 删去后,原图任意两点间最短路不变。

求最多删去多少条边。(\(2\le n\le 300,n-1\le m\le \frac{n(n-1)}{2},1\le w\le 10^9\)\(w\) 表示边权)

考虑转化原题目给出的条件。什么样的边能被删去呢?看起来这个问题似乎不太好像,那我们换一边,什么样的边不能被删去呢?这个问题就比较好解决了,首先,一条边 \((x,y,z)\) 不能被删去的必要条件是 \(dis_{x,y}=z\),其中 \(dis\) 表示最短路。首先肯定有 \(dis_{x,y}\le z\),而如果 \(dis_{x,y}<z\),那最短路上一定没有 \((x,y,z)\),删掉啥都不影响。而另一个必要条件是,\(x,y\) 之间,不存在长度大于 \(1\) 的,权值为 \(z\) 的路径。原因比较显然,因为这样删掉后原图就不连通了。可以 证明,这两个条件加起来就是充要条件了。

必要性已经证明完了,考虑充分性。其实也比较显然,因为这俩条件满足后,删掉这条边会对最短路和连通性造成影响。所以我们现在的问题变为判断两点间最短路,和是否存在长度大于 \(1\),权值为 \(z\) 的路径。这些需要的信息都可以用一遍 \(\rm floyd\) 求出,时间复杂度 \(\mathcal{O}(n^3)\)

#include <cstdio>
#include <cstring>
const int N = 310; typedef long long ll;
ll dis[N][N]; int E[N][N], id[N][N];
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    memset(dis, 0x3f, sizeof (dis));
    for (int i = 1; i <= n; ++i) dis[i][i] = 0;
    for (int i = 1, x, y, z; i <= m; ++i)
        scanf("%d%d%d", &x, &y, &z), E[x][y] = E[y][x] = z,
        id[x][y] = id[y][x] = 1, dis[x][y] = dis[y][x] = z;
    for (int k = 1; k <= n; ++k)
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                if (dis[i][k] + dis[k][j] <= dis[i][j]) 
                    dis[i][j] = dis[i][k] + dis[k][j];
    int cnt = 0;
    for (int i = 1, flg; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
        {
            if (i == j || dis[i][j] != E[i][j]) continue;
            flg = 1;
            for (int k = 1; k <= n; ++k)
                if (k != i && k != j && dis[i][k] + dis[k][j] == E[i][j]) { flg = 0; break; }
            cnt += flg;
        }
    printf("%d\n", m - cnt / 2); return 0;
}

F - Lottery

\(n\) 种奖品,每种奖品有一个权值 \(w_i\),每次抽奖抽到第 \(i\) 个奖品的概率是:

\[\dfrac{w_i}{\sum\limits_{j=1}^nw_j} \]

\(K\) 次抽奖,恰好抽到 \(m\) 种奖品的概率。(\(1\le n,m,K\le 50,m\le n\))

考虑描述一个抽奖的状态,注意到我们关心的是种数,所以考虑用一个序列 \(\left<c\right>\) 来描述:

\[\left<c_1,c_2,c_3,\cdots,c_n\right> \]

\(c_i\) 表示 \(i\) 种物品出现的次数。则这个状态出现的概率是:

\[\dfrac{K!}{\prod\limits_{i=1}^nc_i!}\prod_{i=1}^np_i^{c_i} \]

其中 \(p_i=\frac{w_i}{\sum\limits_{j=1}^nw_j}\),意义就是可重集排列数(一共有几种情况)和朴素的概率。

而注意到,这东西乘起来,每个 \(i\) 的贡献是独立的,可以考虑对每种类型分别考虑,做 \(\rm dp\)

\(f_{i,j,k}\) 表示前 \(i\) 种物品,选了 \(j\) 种,共选了 \(k\) 个物品的,所有方案权值和。其中一种方案 \(\left<c\right>\) 对应的权值:

\[\dfrac{1}{\prod\limits_{i=1}^n c_i!}\prod_{i=1}^n p_i^{c_i} \]

最终答案即为 \(K!f_{n,m,K}\)。转移就是枚举 \(c_i\) 的值:

\[f_{i,j,k}=f_{i-1,j,k}+\sum_{l=1}^k \dfrac{f_{i-1,j-1,k-l}}{l!}p_i^{l} \]

统计答案即可。时间复杂度 \(\mathcal{O}(nmK^2)\)

#include <cstdio>
const int N = 60, mod = 998244353; typedef long long ll;
int W[N], p[N], fac[N], ifac[N], f[N][N][N];
inline int ksm(int a, int b)
{
    int ret = 1;
    while (b)
    {
        if (b & 1) ret = (ll)ret * a % mod;
        a = (ll)a * a % mod; b >>= 1;
    }
    return ret;
}
int main()
{
    int n, m, K, sum = 0; scanf("%d%d%d", &n, &m, &K); fac[0] = 1;
    for (int i = 1; i < N; ++i) fac[i] = (ll)fac[i - 1] * i % mod;
    ifac[N - 1] = ksm(fac[N - 1], mod - 2);
    for (int i = N - 2; i; --i) ifac[i] = (ll)ifac[i + 1] * (i + 1) % mod;
    for (int i = 1; i <= n; ++i) scanf("%d", W + i), (sum += W[i]) %= mod;
    for (int i = 1; i <= n; ++i) p[i] = (ll)W[i] * ksm(sum, mod - 2) % mod;
    f[0][0][0] = 1;
    for (int i = 1; i <= n; ++i)
        for (int j = 0; j <= m; ++j)
            for (int k = 0; k <= K; ++k)
            {
                (f[i][j][k] += f[i - 1][j][k]) %= mod;
                if (!j) continue;
                for (int c = 1; c <= k; ++c)
                    (f[i][j][k] += (ll)f[i - 1][j - 1][k - c] * ifac[c] % mod * ksm(p[i], c) % mod) %= mod;
            }
    int ans = (ll)fac[K] * f[n][m][K] % mod;
    printf("%d\n", ans); return 0;
}

G - Sqrt

\(t\) 组数据,每组给出一个序列 \(A=(x)\),执行一下操作无穷次:

  • 设末尾的数为 \(y\),向末尾加入一个 \([1,\lfloor\sqrt{y}\rfloor]\) 之间的整数。

求最终能得到多少种不同的最终序列。(\(1\le t\le 20,1\le x\le 9\times10^{18}\))

考虑朴素的 \(\rm dp\),设 \(f_x\) 表示以 \(x\) 开头的序列个数,枚举下一次放哪个即可得到朴素转移:

\[f_x=\sum_{i=1}^{\lfloor\sqrt{\overline{x}}\rfloor}f_i \]

处理出所有 \(\le \sqrt{x}\)\(f\),顺便维护个前缀和,即可在 \(\mathcal{O}(t\sqrt{x})\) 的时间复杂度内求解。

当然,这么做是不能通过的,考虑优化。注意到只要我们知道 \(A_i,A_{i+2}\),其实 \(A_{i+1}\) 的范围我们是能知道的。考虑从 \(A_i\) 直接转移到 \(A_{i+2}\),并算上所有 \(A_{i+1}\) 可能的取值。即如果我们假设 \(A_{i+1}\in[l,r]\),则:

\[f_x=\sum_{i=1}^{\lfloor\sqrt[4]{\overline{x}}\rfloor}f_i(r-l+1) \]

容易发现,如果 \(A_i=x,A_{i+2}=i\),则 \(l=i^2,r=\sqrt{x}\)。这样,我们分别维护 \(i^2f_i,f_i\) 的前缀和,即可做到 \(\mathcal{O}(t\sqrt[4]{x})\) 的时间复杂度。

顺便,提前预处理即可做到 \(\mathcal{O}(\sqrt[4]{x})\)。注意,cmathsqrt() 会掉精度,需要处理一下。

#include <cmath>
#include <cstdio>
#include <climits>
const int N = 6e4 + 10; typedef long long ll; 
ll f[N], pref[N], p[N]; typedef long double ld;
inline ll Sqrt(ll x)
{
    ll ret = sqrt(x) + 1;
    while (ret * ret > x) --ret;
    return ret;
}
inline void pre(int n)
{
    f[1] = pref[1] = p[1] = 1; ll t;
    for (ll i = 2; i <= n; ++i)
    {
        t = Sqrt(Sqrt(i));
        f[i] = pref[t] * ((ll)Sqrt(i) + 1) - p[t];
        pref[i] = pref[i - 1] + f[i];
        p[i] = p[i - 1] + f[i] * i * i;
    }
}
int main()
{
    int T; scanf("%d", &T); pre(N - 1);
    while (T--)
    {
        ll x, n; scanf("%lld", &x); n = Sqrt(Sqrt(x));
        printf("%lld\n", pref[n] * ((ll)Sqrt(x) + 1) - p[n]);
    }
    return 0;
}
posted @ 2022-03-25 14:56  zhiyangfan  阅读(61)  评论(0编辑  收藏  举报