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(3\leq n \leq 10^{12})\) ,求迭代到 \(2\) 需要几次?

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

有一种复杂度完全正确的做法,用线性筛筛出 \(2\cdot 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\n", 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\n", ((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\n", 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\n", 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+\sum\limits_{v \in son[u]} cnt0[v]\)

若根u是白点,那么

\(cnt0[u]=1+\sum\limits_{v \in son[u]} cnt0[v]\)
\(cnt1[u]=\sum\limits_{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\n", 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\) 轴上放同理。

posted @ 2020-02-04 19:31  KisekiPurin2019  阅读(251)  评论(0编辑  收藏  举报