CodeVS 第一次月赛 题解

比赛情况

比赛共 254 人提交代码,最高分 250 分,有 102 人 100 分以上。
第一题共 85 人满分,第二题最高分 90,第三题最高分 100。


A 数学奇才琪露诺

题意

s(x) 表示 x 的数位和,给定k,p,q,l,r,求出所有满足 x[l,r] x=s(x)kp+q x
1k5,0p,|q|10000,0lr109

题解

不难发现,对于方程的解 x s(x) x 是唯一对应的,而0s(x)81,显然应该枚举 s(x) 来判断 x 是否是解,时间复杂度O(1)

代码
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long LL;
int k, p, q, l, r, ans[100];
int sum(int x)
{
    int ret = 0;
    while(x)
    {
        ret += x % 10;
        x /= 10;
    }
    return ret;
}
int main()
{
    scanf("%d%d%d%d%d", &k, &p, &q, &l, &r);
    ans[0] = 0;
    for(int s = 0; s <= 81; ++s)
    {
        LL x = p;
        for(int i = 0; i < k; ++i)
            x *= s;
        x += q;
        if(l <= x && x <= r && sum((int)x) == s)
            ans[++ans[0]] = (int)x;
    }
    sort(ans + 1, ans + ans[0] + 1);
    printf("%d\n", ans[0]);
    for(int i = 1; i <= ans[0]; ++i)
        printf("%d%c", ans[i], " \n"[i == ans[0]]);
    return 0;
}

B 完美拓印

题意

给定一个长度为 n 的序列A和一个长度为 m 的序列B,序列 A 代表一个图中的印章,印章的图案不是中心对称的,序列B代表一条边界线,现在问 A 在可以旋转180或者不旋转的情况下,经过平移使得印章的上边缘或是下边缘与边界线完全重合,能拓印出多少种完全不重叠的图案。
n,m106 ,序列元素不超过 109

题解

印章与边界线重合有四种情况,印章的上或下边缘在边界线的上或下方重合,现在来分别分析这四种情况的方案数。
1. 印章的下边缘在边界线的上方或下方重合,即 B 序列里有长度为n的满足元素完全相等的连续子序列对应上下两个方案。
2. 印章的上边缘在边界线的下方重合,即序列 A 的元素加上同一个值之后与B的连续子序列相等对应一个方案。
3. 印章的上边缘在边界线的上方重合,即序列 A 的元素加上同一个值之后与B的连续子序列的逆序列相等。
由于印章的图案不是中心对称的,所以当印章的上边缘完全与下边缘平行时,可能有同样位置的方案,但是印章的图案不重合,直接分别考虑即可。
考虑情况1,由于下边缘是完全水平的,所以只需要看 B 序列里有多少个长度为n的连续子序列是元素完全相等的,由于上下方都可行则方案数再乘 2 即可。可以算出B里的所有互不相交的最长连续相同子序列,对于每个连续的序列,如果其长度 k 不小于n,则对答案贡献 kn+1 个长度为 n 的连续相同子序列。直接扫一遍序列B即可,时间复杂度 O(m)
考虑情况2和3,不难发现如果序列 A 的相邻元素之差等于序列B的子序列 B 的相邻元素之差,则 A B可以重合,或者说是匹配。则问题转化为长度为 n1 的差分序列 dA 和长度为 m1 的差分序列 dB 的匹配问题,其中 dAi=Ai+1Ai,dBi=Bi+1Bi 。可以将 dB dB 的逆序列连接,中间加一个极大元素保证 dA 不会同时匹配到 dB dB 的逆序列,将其作为文本串,将 dA 作为模板串,直接进行kmp匹配,时间复杂度 O(n+m)
注意情况2和3需要特判模板串长度为 0 的情况,即n=1的情况。

代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 1000010;
int n, m, s[maxn << 1], t[maxn], f[maxn], ans;
int main()
{
    scanf("%d%d", &m, &n);
    if(m == 1)
    {
        printf("%d\n", n << 2);
        return 0;
    }
    for(int i = 0; i < m; ++i)
        scanf("%d", t + i);
    for(int i = 0; i < n; ++i)
        scanf("%d", s + i);
    for(int i = 0, j = 0; i < n; i = j)
    {
        while(j < n && s[i] == s[j])
            ++j;
        if(j - i >= m)
            ans += j - i - m + 1 << 1;
    }
    for(int i = --m; i > 0; --i)
        t[i] -= t[i - 1];
    t[0] = 0;
    for(int i = --n; i > 0; --i)
        s[i] -= s[i - 1];
    s[0] = 0;
    s[n + 1] = ~0u >> 1;
    for(int i = 1; i <= n; ++i)
        s[n * 2 + 2 - i] = s[i];
    n = n * 2 + 1;
    for(int i = 2, j = 0; i <= m; ++i)
    {
        while(j > 0 && t[i] != t[j + 1])
            j = f[j];
        if(t[i] == t[j + 1])
            ++j;
        f[i] = j;
    }
    for(int i = 1, j = 0; i <= n; ++i)
    {
        while(j > 0 && s[i] != t[j + 1])
            j = f[j];
        if(s[i] == t[j + 1])
            ++j;
        if(j == m)
        {
            ++ans;
            j = f[j];
        }
    }
    printf("%d\n", ans);
    return 0;
}

C 幻影阁的难题

题意

定义一颗无向树的直径是树上最远两点的距离。给定一颗 n 个点的边有长度的无向树R,一颗 m 个点的边有长度的无向树F,现在可以在两颗树之间添加一条长度为 t 的边将两颗树连接成一颗新的n+m个点的无向树 S ,求最优加边方案能得到的树S直径的最小值,以及等概率随机加边情况下树 S 直径的期望。
n,m200000,任意边的长度不超过 min(1000,4106max(n,m))

题解

对于每个棵树上,可以在 O() 的时间复杂度里,通过两遍dfs完成树形dp,计算出每个节点到其最远点的距离,而一棵树的直径即这些距离的最大值。
考虑一棵树,定义 f1[i] 表示以 i 为根的子树里到i的最远点距离,f2[i]表示以 i 为根的子树里到i的次远点距离, p[i] 表示以 i 为根的子树里到i的最远点所在孩子子树的孩子编号, f3[i] 表示整棵树里到 i 的最远点距离。
f1,f2,p可以通过一遍dfs得出,对于叶子节点 leaf f1[leaf]=f2[leaf]=0,p[leaf]= ,对于非叶节点 u ,只需要考虑孩子节点向下走的最远距离即可。
设其一个孩子为son,距离为 w ,则走孩子son子树的最远距离为 f1[son]+w ,那么 f1[u] 为所有 f1[son]+w 的最大值, p[u] f1[u] 对应的 son f2[u] 为次大值即可,注意到这里 f1[u] 可以等于 f2[u]
考虑再用一遍dfs得到 f3 ,对于树根节点 root f3[root]=f1[root] ,对于非根节点 u ,向孩子走的方案已经包含在f1[u]里了,只需要再考虑向父亲走的情况。
设其父亲为 fa ,距离为 w ,若父亲的最远距离来自u子树,则 u 向上走到fa,再走到其他子树的最远点距离为 f2[fa]+w ,那么 f3[u]=max(f1[u],f2[fa]+w) ,若父亲的最远距离不是来自 u 子树,则u向上走到 fa ,再走到其他子树的最远点距离为 f1[fa]+w ,那么 f3[u]=max(f1[u],f1[fa]+w)
在实际的实现上, f3 可以直接用 f1 代替,具体可以参见代码。
对于加边方案,设其连接的是树 R 的节点i和树 F 的节点j,设其对应的在其树中的到最远点的距离为 f[i],g[j] ,设树 R,F,S 的直径分别为 diaR,diaF,diaS ,则这种连边方案对应的新树 S 满足diaS=max(diaR,diaF,f[i]+g[j]+t)
w=max(diaR,diaF) ,则对于给定的 i ,任意满足f[i]+g[j]+t<w的加边方案,新树的直径 diaS=w ,否则 diaS=f[i]+g[j]+t ,可以发现,将 g[j] 排序后,对应的 j 是一段后缀区间,即jjm,那么可以二分得到 j 。但是我们还可以观察到,如果 i 产生变化导致f[i]变小, j 只会变大或不变,那么将 f[i] 也排序后, i 的递增伴随着j的非增,而j' j 最多减 m 次,所以这段计算直径可以通过两个下标做到O(n+m),但是由于排序的复杂度,整体的时间复杂度还是 O(nlogn) ,但是比二分好写。
由此我们可以通过枚举 i ,变化j的形式得到最小的可能的直径,也可在计算了 g[i] 的前缀和之后快速计算出所有加边方案对应的直径之和,题目的两个问题都得到了解决。

代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = (int)3e5 + 100;
typedef long long LL;
int n, m, t, tot, p[maxn], f[maxn], g[maxn], ff[maxn], pos[maxn], maxl, ans1;
LL sg[maxn], ans21, ans22;
struct Edge
{
    int nxt, v, w;
} e[maxn << 1];
void scan(int &x)
{
    static int ch;
    while((ch = getchar()) < '0' || ch > '9');
    x = ch - '0';
    while((ch = getchar()) >= '0' && ch <= '9')
        x = (x << 3) + (x << 1) + (ch - '0');
}
LL gcd(LL a, LL b)
{
    return b ? gcd(b, a % b) : a;
}
void dfs1(int u, int fa)
{
    for(int it = p[u]; it != -1; it = e[it].nxt)
    {
        int &v = e[it].v, &w = e[it].w;
        if(v == fa)
            continue;
        dfs1(v, u);
        if(f[u] < f[v] + w)
        {
            pos[u] = v;
            ff[u] = f[u];
            f[u] = f[v] + w;
        }
        else if(ff[u] < f[v] + w)
            ff[u] = f[v] + w;
    }
}
void dfs2(int u, int fa)
{
    for(int it = p[u]; it != -1; it = e[it].nxt)
    {
        int &v = e[it].v, &w = e[it].w;
        if(v == fa)
            continue;
        int &pp = pos[u] == v ? ff[u] : f[u];
        if(f[v] < pp + w)
        {
            pos[v] = u;
            ff[v] = f[v];
            f[v] = pp + w;
        }
        else if(ff[v] < pp + w)
            ff[v] = pp + w;
        dfs2(v, u);
    }
}
int dp()
{
    tot = 0;
    memset(p, -1, sizeof p);
    for(int i = 1; i < n; ++i)
    {
        int u, v, w;
        scan(u);
        scan(v);
        scan(w);
        e[tot] = (Edge){p[u], v, w};
        p[u] = tot++;
        e[tot] = (Edge){p[v], u, w};
        p[v] = tot++;
    }
    memset(f, 0, sizeof f);
    memset(ff, 0, sizeof ff);
    memset(pos, 0, sizeof pos);
    dfs1(1, -1);
    dfs2(1, -1);
    sort(f + 1, f + n + 1);
    return f[n];
}
int main()
{
    scan(n);
    scan(m);
    scan(t);
    ans1 = ~0u >> 1;
    ans21 = 0;
    ans22 = (LL)n * m;
    maxl = dp();
    swap(n, m);
    memcpy(g, f, sizeof f);
    maxl = max(maxl, dp());
    memset(sg, 0, sizeof sg);
    for(int i = m; i; --i)
        sg[i] = sg[i + 1] + g[i];
    for(int i = 1, j = m; i <= n; ++i)
    {
        while(j && f[i] + g[j] + t >= maxl)
            --j;
        ans21 += (LL)maxl * j;
        if(j > 0 && ans1 > maxl)
            ans1 = maxl;
        ans21 += (LL)(f[i] + t) * (m - j) + sg[j + 1];
        if(j < m && ans1 > f[i] + g[j + 1] + t)
            ans1 = f[i] + g[j + 1] + t;
    }
    LL r = gcd(ans21, ans22);
    printf("%d\n%lld/%lld\n", ans1, ans21 / r, ans22 / r);
    return 0;
}

一些无脑部分分的设定

  • B 30pts: nm2107 。直接暴力枚举。
  • B 60pts: n,m105 。让Hash+二分、SA过。(会这些还不会kmp?)
  • C 20pts: n,m50 。枚举点,计算两边的直径,及其他乱搞大法。
  • C 40pts: n,m500 。枚举点,计算新树直径。
  • C 60pts: n,m5000 。暴力预处理两边的最远点,暴力枚举加边方案。

为了NOIP提高组难度而设定的部分分真是不忍直视。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值