• 2020牛客寒假算法基础集训营1


    题目链接:https://ac.nowcoder.com/acm/contest/3002
    题解链接:https://ac.nowcoder.com/discuss/364600

    B - kotori和bangdream

    水题。

    D - hanayo和米饭

    水题。

    E - rin和快速迭代

    题意:设 (f(x))(x) 的因数的个数,猜想每次用 (x:=f(x)) 迭代下去,所有正整数最后都会变成 (2) 。每次输入一个 (n(3leq n leq 10^{12})) ,求迭代到 (2) 需要几次?

    题解:首先 (f(1)=1) 猜想不成立……不过题目输入的 (n(3leq n leq 10^{12})) ,显然当 (n) 为奇质数的时候只需要1次,而假如 (n) 是合数则会变小为至多 (O(sqrt{n})) ,单次分解是 (O(sqrt{n})) 的,这个求和貌似是 (O(n^{frac{3}{4}})) ,而且因数的个数实际上远小于 (O(sqrt{n}))

    有一种复杂度完全正确的做法,用线性筛筛出 (2cdot 10^6) 以内每个数的因数的个数。同时用dp方程 (dp[f(x)]=dp[x]+1) 求出每个数需要的迭代次数。最后对 (n) 进行一次分解即可。胆子更大的话可以把更大的 (n) 用 Pollard_rho 分解。

    int fac(ll x) {
        int cnt = 0;
        for(ll i = 1; i * i <= x; ++i) {
            if(x % i == 0) {
                ++cnt;
                if(i * i != x)
                    ++cnt;
            }
        }
        return cnt;
    }
    
    void test_case() {
        ll n;
        scanf("%lld", &n);
        int res = fac(n);
        int cnt = 1;
        while(res != 2) {
            n = res;
            res = fac(n);
            ++cnt;
        }
        printf("%d
    ", cnt);
    }
    

    G - eli和字符串

    水题。

    *A - honoka和格点三角形

    吐槽:这个有点复杂的计数题居然过得这么多,看来我国icpc的计数水平比我高很多。

    题意:取 (n*m) 个整数点构成矩阵,用其中的三个点构成三角形。并要求:

    1、三角形的面积为1;

    2、三角形至少有一边与x轴或y轴平行;

    吐槽:一开始看成“三角形有两边与x轴或y轴平行”,还好过不了样例,然后搞了好几个假算法演了几次。

    题解:首先把三角形与x轴或y轴平行的那条边称为“底”,那么剩下的那个点到“底”的距离就是“高”,由于都是整点所以底只能是1或2,对应的高必须是2或1。

    选择这样的三角形分几步:

    1、确定“底”平行于那条轴,以及“底”的长度

    2、确定“高”的指向,是指从“底”指向剩下的那个点的方向,这个会对“底”能选的直线产生限制。

    3、确定“底”所在的直线

    4、确定“底”在直线上具体的位置

    5、确定“高”的位置,也就是剩下的那个点的位置

    以上各步独立,用分步乘法计数原理。

    最后因为有的三角形既和x轴平行又和y轴平行,会被计数两次,这样的三角形是直角边分别为1和2的直角三角形,把它们去掉。

    void test_case() {
        ll n, m;
        scanf("%lld%lld", &n, &m);
        ll res1 = ((n - 1) * (m - 2)) % MOD * m % MOD * 2 % MOD;
        ll res2 = ((m - 1) * (n - 2)) % MOD * n % MOD * 2 % MOD;
        ll res3 = ((n - 2) * (m - 1)) % MOD * m % MOD * 2 % MOD;
        ll res4 = ((m - 2) * (n - 1)) % MOD * n % MOD * 2 % MOD;
        ll res5 = ((n - 1) * (m - 2)) % MOD * 4 % MOD;
        ll res6 = ((m - 1) * (n - 2)) % MOD * 4 % MOD;
        printf("%lld
    ", ((res1 + res2 + res3 + res4 - res5 - res6) % MOD + MOD) % MOD);
    }
    

    H - nozomi和字符串

    水题,但是怎么比A过得还少,可能我科技树点歪了。

    题意:有一个01串,每次操作可以翻转一个位置,进行至多 (k) 次操作,求能构成的最长的一段连续相同的子串。

    题解:这个显然可以二分,只需要想怎么check。枚举长度len之后,可以尺取转移出当前段内的0的数量和1的数量,只需要其中一种<=k就可以直接全部翻转。

    *I - nico和niconiconi

    题意:给一个字符串,其中“nico”可以获得得分 (a) ,“niconi”可以获得得分 (b) ,“niconiconi”可以获得得分 (c) ,注意不能够重复获得得分!例如“niconico”要么当作“nico”+“nico”计 (2a) 分,要么当作“niconi”+“co”计 (c) 分。

    题解:看起来就很像匹配字符串的一个自动机?假如知道当前匹配的长度,那么能够去往的下一个状态必定是唯一的。但是正确设出状态需要动一下脑子。

    (dp[i][j]) 为前 (i) 个字符当前的末尾有 (j) 个字符未获得得分,由于题目的三个串依次是前缀,所以一共有 ([0,10]) 这么多种匹配长度。

    规定1:只有当某一步把匹配长度恰好为4,6和10的状态消去才能获得对应的得分!

    那么怎么解决一个字符串同时匹配多种得分组合的情况呢?只需要

    规定2:任何时候都可以主动截断现在的已匹配长度,获得对应的得分!

    这样在完成一次匹配的时候会自动更新对应的状态。

    最后,小心在传参的时候溢出,而且为了更快实现规定2所需的操作,维护一个前一个字符各个状态的最大值就可以了。

    int n, a, b, c;
    char s[300005];
    
    ll dp[300005][11];
    
    ll max3(ll a, ll b, ll c) {
        return max(a, max(b, c));
    }
    
    ll max4(ll a, ll b, ll c, ll d) {
        return max(max(a, b), max(c, d));
    }
    
    void test_case() {
        scanf("%d%d%d%d%s", &n, &a, &b, &c, s + 1);
        ll premax = 0;
        for(int j = 1; j <= 10; ++j)
            dp[0][j] = -LINF;
        for(int i = 1; i <= n; ++i) {
            if(s[i] == 'n') {
                dp[i][0] = max4(premax, dp[i - 1][4] + a, dp[i - 1][6] + b, dp[i - 1][10] + c);
                dp[i][1] = dp[i][0];
                dp[i][2] = -LINF;
                dp[i][3] = -LINF;
                dp[i][4] = -LINF;
                dp[i][5] = max(dp[i - 1][4], dp[i - 1][8]);
                dp[i][6] = -LINF;
                dp[i][7] = -LINF;
                dp[i][8] = -LINF;
                dp[i][9] = dp[i - 1][8];
                dp[i][10] = -LINF;
            } else if(s[i] == 'i') {
                dp[i][0] = max4(premax, dp[i - 1][4] + a, dp[i - 1][6] + b, dp[i - 1][10] + c);
                dp[i][1] = -LINF;
                dp[i][2] = max3(dp[i - 1][1], dp[i - 1][5], dp[i - 1][9]);
                dp[i][3] = -LINF;
                dp[i][4] = -LINF;
                dp[i][5] = -LINF;
                dp[i][6] = max(dp[i - 1][5], dp[i - 1][9]);
                dp[i][7] = -LINF;
                dp[i][8] = -LINF;
                dp[i][9] = -LINF;
                dp[i][10] = dp[i - 1][9];
            } else if(s[i] == 'c') {
                dp[i][0] = max4(premax, dp[i - 1][4] + a, dp[i - 1][6] + b, dp[i - 1][10] + c);
                dp[i][1] = -LINF;
                dp[i][2] = -LINF;
                dp[i][3] = max3(dp[i - 1][2], dp[i - 1][6], dp[i - 1][10]);
                dp[i][4] = -LINF;
                dp[i][5] = -LINF;
                dp[i][6] = -LINF;
                dp[i][7] = max(dp[i - 1][6], dp[i - 1][10]);
                dp[i][8] = -LINF;
                dp[i][9] = -LINF;
                dp[i][10] = -LINF;
            } else if(s[i] == 'o') {
                dp[i][0] = max4(premax, dp[i - 1][4] + a, dp[i - 1][6] + b, dp[i - 1][10] + c);
                dp[i][1] = -LINF;
                dp[i][2] = -LINF;
                dp[i][3] = -LINF;
                dp[i][4] = max(dp[i - 1][3], dp[i - 1][7]);
                dp[i][5] = -LINF;
                dp[i][6] = -LINF;
                dp[i][7] = -LINF;
                dp[i][8] = dp[i - 1][7];
                dp[i][9] = -LINF;
                dp[i][10] = -LINF;
            } else {
                dp[i][0] = max4(premax, dp[i - 1][4] + a, dp[i - 1][6] + b, dp[i - 1][10] + c);
                dp[i][1] = -LINF;
                dp[i][2] = -LINF;
                dp[i][3] = -LINF;
                dp[i][4] = -LINF;
                dp[i][5] = -LINF;
                dp[i][6] = -LINF;
                dp[i][7] = -LINF;
                dp[i][8] = -LINF;
                dp[i][9] = -LINF;
                dp[i][10] = -LINF;
            }
            premax = -INF;
            for(int j = 0; j <= 10; ++j) {
                //printf("dp[%d][%d]=%lld
    ", i, j, dp[i][j]);
                premax = max(premax, dp[i][j]);
            }
            //puts("");
        }
        ll ans = max4(premax, dp[n][4] + a, dp[n][6] + b, dp[n][10] + c);
        printf("%lld
    ", ans);
    }
    

    其中应该有些转移是多余的,但是无所谓。

    *F - maki和tree

    挺有意思的一个简单树形dp。启示来源于树分治的例题中把路径分成几类的的方法。

    题意:给一棵树,每个点或黑或白,求有多少条长度>=2的路径,满足恰好只经过一个黑点。这里路径的长度定义为经过的点的数量。

    题解:首先特判掉n<=2的奇异情况,以后每次做树的题我都先特判掉奇异情况。然后这样的树就必定有至少一个非叶子的点,选第一个这样的点作为根,这样根就不会是叶子了(虽然在这道题并不影响,不过可能是昨晚一道树形题的后遗症)。

    然后定好根之后,每条路径必定满足下面两种之一(规定u的深度不深于v的深度):

    1、路径(u,v),其中lca(u,v)=u,即从u向v向下走的路。

    2、路径(u,v),其中lca(u,v)!=u,可以分解为从lca向u和向v向下走的两条路。

    也就是type2的类型可以分解为两条向下路径的组合。需要维护的就是从当前的根向下走的,没有经过任何黑点的路 (cnt0) ,以及恰好只经过一个黑点的路 (cnt1)

    很明显这个东西假如从叶子开始合并子树的话,每次只受新加入的根的影响。具体为:

    若根u是黑点,那么

    (cnt0[u]=0)
    (cnt1[u]=1+sumlimits_{v in son[u]} cnt0[v])

    若根u是白点,那么

    (cnt0[u]=1+sumlimits_{v in son[u]} cnt0[v])
    (cnt1[u]=sumlimits_{v in son[u]} cnt1[v])

    维护的方法已经有了,那么按照上面的路径种类分别计数,type1的直接由 (cnt1[u]) 的定义得就是这个东西,不过要减掉终点也是根的那个1。

    type2的,若根u是黑点,那么应该选两条没有经过任何黑点的路,且这两条路走向不同的子树。若根u是白点,那么应该选一条没有经过任何黑点的路,和一条恰好只经过一个黑点的路,且这两条路走向不同的子树。

    char s[100005];
    int color[100005];
    vector<int> G[100005];
    
    int cnt0[100005], cnt1[100005];
    
    ll sum;
    
    void dfs(int u, int p) {
        if(G[u].size() == 1) {
            if(color[u]) {
                cnt0[u] = 0;
                cnt1[u] = 1;
            } else {
                cnt0[u] = 1;
                cnt1[u] = 0;
            }
            return;
        }
        int sumcnt0 = 0;
        int sumcnt1 = 0;
        for(auto &v : G[u]) {
            if(v == p)
                continue;
            dfs(v, u);
            sumcnt0 += cnt0[v];
            sumcnt1 += cnt1[v];
        }
        if(color[u]) {
            sum += sumcnt0;
            ll tmp = 0;
            for(auto &v : G[u]) {
                if(v == p)
                    continue;
                tmp += 1ll * (sumcnt0 - cnt0[v]) * (cnt0[v]);
            }
            sum += tmp / 2;
            cnt0[u] = 0;
            cnt1[u] = 1 + sumcnt0;
            return;
        } else {
            sum += sumcnt1;
            ll tmp = 0;
            for(auto &v : G[u]) {
                if(v == p)
                    continue;
                tmp += 1ll * (sumcnt1 - cnt1[v]) * (cnt0[v]);
            }
            sum += tmp;
            cnt0[u] = 1 + sumcnt0;
            cnt1[u] = sumcnt1;
            return;
        }
    }
    
    void test_case() {
        int n;
        scanf("%d%s", &n, s + 1);
        for(int i = 1; i <= n; ++i) {
            if(s[i] == 'B')
                color[i] = 1;
        }
        for(int i = 1; i <= n - 1; ++i) {
            int u, v;
            scanf("%d%d", &u, &v);
            G[u].push_back(v);
            G[v].push_back(u);
        }
        if(n == 1) {
            puts("0");
            return;
        }
        if(n == 2) {
            if(color[1] + color[2] == 1) {
                puts("1");
                return;
            }
            puts("0");
            return;
        }
        int r = -1;
        for(int i = 1; i <= n; ++i) {
            if(G[i].size() != 1) {
                r = i;
                break;
            }
        }
        sum = 0;
        dfs(r, -1);
        printf("%lld
    ", sum);
    }
    

    反思:看了题解原来是数每个白点连通块,然后相邻两个连通块之间乘法选就可以了。不需要搞这么复杂233。

    J - u's的影响力

    可以把递推式写出来,发现指数满足某种线性递推的dp规律,所以可以用矩阵快速幂或者BM来解出正确的指数,然后套个快速幂就有答案。

    C - umi和弓道

    题意:在 (Oxy) 平面上给一个原点 ((x_0,y_0)) ,要在原点射箭(射线)出去。再给 (n) 个位置两两不同也和原点不同的点作为箭靶,且这 (n+1) 个点都不在坐标轴上。在坐标轴上画一条尽可能短的隔板 (seg) ,使得原点能射到的箭靶的数量不超过 (k)

    题解:先考虑在 (x) 轴上放,那么只能隔开 (y) 与原点的 (y_0) 异号的箭靶,显然要把他们之间的连线在 (x) 轴上的交点覆盖住,所以求出交点之后之间尺取就可以。在 (y) 轴上放同理。

  • 相关阅读:
    第十三周作业
    第十二周作业2
    第十二周作业
    第十一次作业
    第十周作业
    第九周作业
    第十五次作业
    十四周上机作业
    第十三周上机作业
    第十二周作业
  • 原文地址:https://www.cnblogs.com/KisekiPurin2019/p/12260723.html
Copyright © 2020-2023  润新知