zoukankan      html  css  js  c++  java
  • 「THP3考前信心赛」题解


    写在前面

    比赛地址:THP3 考前信心赛

    感谢原出题人的贡献:第一题 CF1422C,第四题 CF1422D。

    所有题目背景均出自 《秘封俱乐部》系列专辑附带故事,感谢太田顺也先生的创造。

    感谢 KersenxwmwrYpayRainycolor_Mahou 的鼎力相助。

    超级亲民的一场,大片部分分,没有任何高级算法技巧,有一车大样例。

    A 未来宇宙

    给定一长度为 (n) 的只由 (0sim 9) 构成的字符串,求删除一个任意非空子串后得到的所有十进制数 的和。
    答案对 (10^9 + 7) 取模。
    (1le nle 2 imes 10^6)
    1S,128MB。

    定义 (f(l,r)) 表示子串 ([l,r]) 组成的十进制数。
    考虑枚举删除的子串的最后一位 (x),得到的十进制数的和为:

    [egin{aligned} &sum_{i=1}^{x-1}{left{ 10^{n-x} imes f(1,i) +f(x+1,n) ight}}\ =& (x-1) imes f(x+1,n) + 10^{n-x} imes sum_{i=1}^{x-1}{f(1,i)}\ end{aligned}]

    预处理出后缀表示的十进制数,枚举 (x) 的时候维护出前缀十进制数的和即可。
    预处理 (10^?) 后时间复杂度 (O(n))

    //知识点:瞎搞
    /*
    By:Luckyblock
    */
    #include <cctype>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #define LL long long
    const int kN = 2e6 + 10;
    const LL mod = 1e9 + 7;
    //=============================================================
    int n;
    char s[kN];
    LL ans, sum, pow10[kN], suf[kN];
    //=============================================================
    inline int read() {
      int f = 1, w = 0; char ch = getchar();
      for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
      for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
      return f * w;
    }
    //=============================================================
    int main() {
      scanf("%s", s + 1);
      n = strlen(s + 1);
      pow10[0] = 1;
      for (int i = 1; i <= n; ++ i) {
        pow10[i] = pow10[i - 1] * 10ll % mod;;
      }
      for (int i = n; i >= 1; -- i) {
        suf[i] = suf[i + 1];
        suf[i] += (s[i] - '0') * pow10[n - i] % mod;
        suf[i] %= mod;
      }
      LL val = 0;
      for (int i = 1; i <= n; ++ i) {
        ans += sum * pow10[n - i] % mod + (i - 1) * suf[i] % mod;
        ans %= mod;
        val = (10ll * val % mod + s[i] - '0') % mod;
        sum = (sum + val) % mod;
      }
      printf("%lld
    ", ans);
      return 0;
    }
    

    B 空海澄澈

    定义:

    [f(n) = sum_{i=1}^{n}gcd(i,n) ]

    (m) 次询问,每次询问给定参数 (l,r),求:

    [sum_{i=l}^{r}f(i)pmod {998244353} ]

    (1le mle 10^6)(1le l le rle 10^6)
    1S,128MB。

    这个式子是 LuckyblockP5518 [MtOI2019]幽灵乐团 的时候化出来的,因为比较基础,所以就拿过来用了。

    考虑化一下 (f)

    [f(n) = sum_{i=1}^{n}gcd(i,n) ]

    考虑对于每一个 (1sim n) 的值,能作为多少数对的 (gcd),于是有:

    [f(n) = sum_{i=1}^{n} d sum_{i=1}^{n}[gcd(i,n) = d] ]

    发现 (gcd(i,n) = d) 的必要条件是 (d|n),原式可以改为:

    [f(n) = sum_{d|n} dsum_{i=1}^{n}[gcd(i,n) =d] ]

    考虑什么样的 (i),满足 (gcd(i,n) = d),显然当且仅当 (i=kd(kin mathbb{N^*})),且 (gcd(k,frac{n}{d})=1) 时满足条件。为保证 (ile n),有 (k le leftlfloorfrac{n}{d} ight floor)
    于是考虑把 (d) 提出来,改为枚举上述的 (k),原式等于:

    [f(n) = sum_{d|n} d sum_{k=1}^{leftlfloorfrac{n}{d} ight floor}left[gcdleft(k,frac{n}{d} ight)=1 ight] ]

    考虑后面一个 (sum) 的实际含义,表示 (1sim frac{n}{d}) 中与 (frac{n}{d}) 互质的数的个数,符合欧拉函数的定义,于是原式等于:

    [f(n) = sum_{d|n} d cdot varphileft(frac{n}{d} ight) ]

    线性筛预处理 (varphi) 后,用埃氏筛即可筛出 (1sim 10^6) 的所有 (f)
    做个前缀和即可回答区间询问。

    复杂度 (O(nlog n + m))

    //知识点:数论
    /*
    By:Luckyblock
    */
    #include <algorithm>
    #include <cctype>
    #include <cstdio>
    #include <cstring>
    #define LL long long
    const int kN = 1e6 + 10;
    const int kMax = 1e6;
    const int mod = 998244353;
    //=============================================================
    int p_num, p[kN], phi[kN];
    int f[kN], sum[kN];
    bool vis[kN];
    //=============================================================
    inline int read() {
      int f = 1, w = 0;
      char ch = getchar();
      for (; !isdigit(ch); ch = getchar())
        if (ch == '-') f = -1;
      for (; isdigit(ch); ch = getchar()) {
        w = (w << 3) + (w << 1) + (ch ^ '0');
      }
      return f * w;
    }
    void Chkmax(int &fir_, int sec_) {
      if (sec_ > fir_) fir_ = sec_;
    }
    void Chkmin(int &fir_, int sec_) {
      if (sec_ < fir_) fir_ = sec_;
    }
    void Init() {
      phi[1] = 1;
      for (int i = 2; i <= kMax; ++ i) {
        if (! vis[i]) {
          p[++ p_num] = i;
          phi[i] = i - 1;
        }
        for (int j = 1; j <= p_num && i * p[j] <= kMax; ++ j) {
          vis[i * p[j]] = true;
          if (i % p[j] == 0) {
            phi[i * p[j]] = phi[i] * p[j];
            break;
          }
          phi[i * p[j]] = phi[i] * (p[j] - 1);
        }
      }
      
      for (int i = 1; i <= kMax; ++ i) {
        for (int j = i; j <= kMax; j += i) {
          f[j] += 1ll * phi[i] * (j / i) % mod;
          f[j] %= mod;
        }
      }
      for (int i = 1; i <= kMax; ++ i) {
        sum[i] = (sum[i - 1] + f[i]) % mod;
      }
    }
    //=============================================================
    int main() {
      Init();
      int m = read();
      while (m --) {
        int l = read(), r = read();
        printf("%d
    ", (sum[r] - sum[l - 1] + mod) % mod);
      }
      return 0;
    }
    

    C 旧约酒馆

    给定两周长为 (n)(01) 环。
    将它们叠放在一起,可以随意旋转两个环。
    定义一种放置方案的价值为两个环对应位置的与的和,求最大价值。
    (1le nle 5 imes 10^4)
    1S,128MB。

    这题是今年 10 月份去刷题班的晚上偷着听 mp3 的时候想出来的。
    那个 mp3 就如题面所说,可以通过旋转获得不同的音量大小,实在是太厉害了= =

    算法一

    先把两个环展开,固定其中一个环,枚举另一个环的最多 (n) 种形态。
    使用 bool 数组记录形态,暴力与起来求答案,复杂度 (O(n^2))

    算法二

    发现如果 (n) 较小的话,可以直接用整形变量存下展开后的环。
    修改环的形态,可以在前一个形态的基础上通过位运算简单得到。
    取与操作的复杂度也变得很小,总复杂度仅为 (O(n)) 级别。

    考虑把一长度为 (n)bool 数组压成一长度为 (frac{n}{64})unsigned long long 数组。
    形态变化可以通过右移和赋值完成,取与时对每一个数分别取与,并求 (1) 的个数。
    总复杂度 (Oleft(dfrac{n^2}{64} ight))


    C++ 中提供了一个与上述过程实现类似的容器,叫做 bitset
    可以将 bitset 当做一个支持取交并的 bool 数组使用。
    需要注意的是 bitset 的时空复杂度是与系统位数有关的。

    是一个非常简单的小工具,详细请看:OI-Wiki
    以下是用 bitset 实现的代码:

    //
    /*
    By:Luckyblock
    */
    #include <algorithm>
    #include <cctype>
    #include <cstdio>
    #include <cstring>
    #include <bitset>
    #define LL long long
    const int kMaxn = 1e5 + 10;
    //=============================================================
    int n, ans;
    char sa[kMaxn], sb[kMaxn];
    std::bitset <kMaxn> a, b, c;
    //=============================================================
    inline int read() {
      int f = 1, w = 0;
      char ch = getchar();
      for (; !isdigit(ch); ch = getchar())
        if (ch == '-') f = -1;
      for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
      return f * w;
    }
    void Chkmax(int &fir_, int sec_) {
      if (sec_ > fir_) fir_ = sec_;
    }
    void Chkmin(int &fir_, int sec_) {
      if (sec_ < fir_) fir_ = sec_;
    }
    //=============================================================
    int main() {
      n = read();
      scanf("%s", sa + 1); 
      scanf("%s", sb + 1);
      for (int i = 1; i <= n; ++ i) {
        a[i] = (sa[i] == '1');
        b[i] = (sb[i] == '1');
      }
      
      for (int i = 1; i <= n; ++ i) {
        b[n + 1] = b[1];
        b >>= 1;
        Chkmax(ans, (a & b).count());
      }
      printf("%d
    ", ans);
      return 0;
    }
    

    D 博物之志

    给定一 (n imes n) 的网格,图中有 (m) 个给定的关键点。
    给定人的起点终点,每次可以向上下左右任意方向移动一格。
    特别地,当人与一个关键点横坐标相同或纵坐标相同时,可以瞬移到关键点,不花费次数。
    求从起点到终点的最小移动次数。
    (1le nle 10^9)(1le mle 10^5)
    1S,256MB。

    算法一

    有个显然的暴力,每个点向上下左右的点连权值为 1 的双向边。每个关键点向同行同列的点连权值为 1 的双向边。然后跑 Dijkstra。
    点数边数是 (O(n^2)) 级别的,时间复杂度 (O(n^2log (n^2))) 级别,期望得分 30pts。

    注意特判一下 task1。

    //知识点:建图,最短路 
    /*
    By:Luckyblock
    */
    #include <algorithm>
    #include <cctype>
    #include <cstdio>
    #include <cstring>
    #include <queue>
    #define pr std::pair
    #define mp std::make_pair 
    #define LL long long
    const int kM = 1e6 + 10;
    const int kE = 6e6 + 10;
    //=============================================================
    int n, m, sx, sy, tx, ty;
    int e_num, head[kM], v[kE], w[kE], ne[kE];
    LL dis[kM];
    bool vis[kM];
    //=============================================================
    inline int read() {
      int f = 1, w = 0;
      char ch = getchar();
      for (; !isdigit(ch); ch = getchar())
        if (ch == '-') f = -1;
      for (; isdigit(ch); ch = getchar()) {
        w = (w << 3) + (w << 1) + (ch ^ '0');
      }
      return f * w;
    }
    void Chkmax(int &fir_, int sec_) {
      if (sec_ > fir_) fir_ = sec_;
    }
    void Chkmin(int &fir_, int sec_) {
      if (sec_ < fir_) fir_ = sec_;
    }
    void AddEdge(int u_, int v_, int w_) {
      v[++ e_num] = v_;
      w[e_num] = w_;
      ne[e_num] = head[u_];
      head[u_] = e_num;
    }
    void Dijkstra(int s_) {
      std::priority_queue <pr <LL, int> > q;
      memset(dis, 63, sizeof (dis));
      memset(vis, 0, sizeof (vis));
      dis[s_] = 0;
      q.push(mp(0, s_));
      
      while (! q.empty()) {
        int u_ = q.top().second;
        q.pop();
        if (vis[u_]) continue ;
        vis[u_] = true;
        for (int i = head[u_]; i; i = ne[i]) {
          int v_ = v[i], w_ = w[i];
          if (dis[u_] + w_ < dis[v_]) {
            dis[v_] = dis[u_] + w_;
            q.push(mp(-dis[v_], v_));
          }
        }
      }
    }
    int Id(int x_, int y_) {
      return (x_ - 1) * n + y_;
    }
    //=============================================================
    int main() {
      n = read(), m = read();
      sx = read(), sy = read();
      tx = read(), ty = read();
      if (m == 0) {
        printf("%d
    ", abs(tx - sx) + abs(ty - sy));
        return 0;
      }
      for (int i = 1; i <= n; ++ i) {
        for (int j = 1; j <= n; ++ j) {
          if (i + 1 <= n) AddEdge(Id(i, j), Id(i + 1, j), 1);
          if (j + 1 <= n) AddEdge(Id(i, j), Id(i, j + 1), 1);
          if (i - 1 > 0) AddEdge(Id(i, j), Id(i - 1, j), 1);
          if (j - 1 > 0) AddEdge(Id(i, j), Id(i, j - 1), 1);
        }
      }
      for (int i = 1; i <= m; ++ i) {
        int x = read(), y = read();
        for (int j = 1; j <= n; ++ j) {
          AddEdge(Id(x, j), Id(x, y), 0); 
          AddEdge(Id(j, y), Id(x, y), 0);
        }
      }
      Dijkstra(Id(sx, sy));
      printf("%lld
    ", dis[Id(tx, ty)]);
      return 0;
    }
    

    算法二

    (n) 这么大,显然标算是个与 (n) 无关的算法。

    考虑从起点到终点的最短路径。
    若不经过任何一个关键点,最短路即为两点曼哈顿距离,可以直接算出。
    否则可以把最短路看成:起点 ( ightarrow) 关键点 ( ightarrow) 终点。
    于是将关键点作为中继点,改变连边方式:

    • 起点向关键点连边,权值为 (min(|sx-x|, |sy-y|))
    • 关键点与关键点之间连 双向 边,权值为 (min(|x_1-x_2|, |y_1-y_2|))
    • 关键点向终点连边,权值为曼哈顿距离。

    再跑 Dijkstra,点数边数变为 (O(m^2)) 级别,时间复杂度 (O(m^2 log (m^2))) 级别,期望得分 70pts。

    //知识点:建图,最短路 
    /*
    By:Luckyblock
    */
    #include <algorithm>
    #include <cctype>
    #include <cstdio>
    #include <cstring>
    #include <queue>
    #define pr std::pair
    #define mp std::make_pair 
    #define LL long long
    const int kM = 1e5 + 10;
    const int kE = 6e6 + 10;
    //=============================================================
    struct Node {
      int x, y;
    } a[kM];
    int n, m, sx, sy, tx, ty;
    int e_num, head[kM], v[kE], w[kE], ne[kE];
    LL dis[kM];
    bool vis[kM];
    //=============================================================
    inline int read() {
      int f = 1, w = 0;
      char ch = getchar();
      for (; !isdigit(ch); ch = getchar())
        if (ch == '-') f = -1;
      for (; isdigit(ch); ch = getchar()) {
        w = (w << 3) + (w << 1) + (ch ^ '0');
      }
      return f * w;
    }
    void Chkmax(int &fir_, int sec_) {
      if (sec_ > fir_) fir_ = sec_;
    }
    void Chkmin(int &fir_, int sec_) {
      if (sec_ < fir_) fir_ = sec_;
    }
    void AddEdge(int u_, int v_, int w_) {
      v[++ e_num] = v_;
      w[e_num] = w_;
      ne[e_num] = head[u_];
      head[u_] = e_num;
    }
    void Dijkstra(int s_) {
      std::priority_queue <pr <LL, int> > q;
      memset(dis, 63, sizeof (dis));
      memset(vis, 0, sizeof (vis));
      dis[s_] = 0;
      q.push(mp(0, s_));
      
      while (! q.empty()) {
        int u_ = q.top().second;
        q.pop();
        if (vis[u_]) continue ;
        vis[u_] = true;
        for (int i = head[u_]; i; i = ne[i]) {
          int v_ = v[i], w_ = w[i];
          if (dis[u_] + w_ < dis[v_]) {
            dis[v_] = dis[u_] + w_;
            q.push(mp(-dis[v_], v_));
          }
        }
      }
    }
    //=============================================================
    int main() {
      n = read(), m = read();
      sx = read(), sy = read();
      tx = read(), ty = read();
      AddEdge(0, m + 1, abs(tx - sx) + abs(ty - sy));
      for (int i = 1; i <= m; ++ i) {
        int x = read(), y = read();
        a[i] = (Node) {x, y};
        AddEdge(0, i, std::min(abs(sx - x), abs(sy - y)));
        AddEdge(i, m + 1, abs(tx - x) + abs(ty - y));
      }
      for (int i = 1; i <= m; ++ i) {
        for (int j = i + 1; j <= m; ++ j) {
          AddEdge(i, j, std::min(abs(a[i].x - a[j].x), abs(a[i].y - a[j].y)));
          AddEdge(j, i, std::min(abs(a[i].x - a[j].x), abs(a[i].y - a[j].y)));
        }
      }
      Dijkstra(0);
      printf("%lld
    ", dis[m + 1]);
      return 0;
    }
    

    算法三

    为表达方便,以下钦定两关键点间的距离为 (min(|x_1-x_2|, |y_1-y_2|))

    考虑三个关键点之间的连边,如果出现下图情况:

    显然 (A ightarrow C) 的距离不小于 (A ightarrow B)(B ightarrow C) 的距离之和。
    因此可以不连 (A ightarrow C) 的边,不会影响 (A ightarrow C) 的最短路,可以删除这条边。

    再考虑更一般的情况,如果有下图:

    (A ightarrow C) 的距离仍然不小于 (A ightarrow B)(B ightarrow C) 的距离之和。
    因此可以不连 (A ightarrow C) 的边,不会影响 (A ightarrow C) 的最短路。
    但注意到 (A ightarrow C) 的边会对 (A ightarrow B) 的最短路作出贡献,这条边不能删除。

    于是得到一个对算法二的优化:
    先把关键点按 (x) 坐标排序,在排序后相邻两个点连 双向边。再把关键点按 (y) 坐标排序,在排序后相邻两点连 双向边
    跑出来的最短路与之前的相等,但点数边数仅为 (O(m)) 级别,时间复杂度 (O(mlog m)) 级别,可以通过。

    注意空间大小。

    //知识点:建图,最短路 
    /*
    By:Luckyblock
    */
    #include <algorithm>
    #include <cctype>
    #include <cstdio>
    #include <cstring>
    #include <queue>
    #define pr std::pair
    #define mp std::make_pair 
    #define LL long long
    const int kM = 1e5 + 10;
    const int kE = 6e5 + 10;
    //=============================================================
    struct Node {
      int x, y, id;
    } a[kM];
    int n, m, sx, sy, tx, ty;
    int e_num, head[kM], v[kE], w[kE], ne[kE];
    LL dis[kM];
    bool vis[kM];
    //=============================================================
    inline int read() {
      int f = 1, w = 0;
      char ch = getchar();
      for (; !isdigit(ch); ch = getchar())
        if (ch == '-') f = -1;
      for (; isdigit(ch); ch = getchar()) {
        w = (w << 3) + (w << 1) + (ch ^ '0');
      }
      return f * w;
    }
    void Chkmax(int &fir_, int sec_) {
      if (sec_ > fir_) fir_ = sec_;
    }
    void Chkmin(int &fir_, int sec_) {
      if (sec_ < fir_) fir_ = sec_;
    }
    bool CMPx(Node fir_, Node sec_) {
      return fir_.x < sec_.x;
    }
    bool CMPy(Node fir_, Node sec_) {
      return fir_.y < sec_.y;
    }
    void AddEdge(int u_, int v_, int w_) {
      v[++ e_num] = v_;
      w[e_num] = w_;
      ne[e_num] = head[u_];
      head[u_] = e_num;
    }
    void Dijkstra(int s_) {
      std::priority_queue <pr <LL, int> > q;
      memset(dis, 63, sizeof (dis));
      memset(vis, 0, sizeof (vis));
      dis[s_] = 0;
      q.push(mp(0, s_));
      
      while (! q.empty()) {
        int u_ = q.top().second;
        q.pop();
        if (vis[u_]) continue ;
        vis[u_] = true;
        for (int i = head[u_]; i; i = ne[i]) {
          int v_ = v[i], w_ = w[i];
          if (dis[u_] + w_ < dis[v_]) {
            dis[v_] = dis[u_] + w_;
            q.push(mp(-dis[v_], v_));
          }
        }
      }
    }
    //=============================================================
    int main() {
      n = read(), m = read();
      sx = read(), sy = read();
      tx = read(), ty = read();
      AddEdge(0, m + 1, abs(tx - sx) + abs(ty - sy));
      for (int i = 1; i <= m; ++ i) {
        int x = read(), y = read();
        a[i] = (Node) {x, y, i};
        AddEdge(0, i, std::min(abs(sx - x), abs(sy - y)));
        AddEdge(i, m + 1, abs(tx - x) + abs(ty - y));
      }
      std::sort(a + 1, a + m + 1, CMPx);
      for (int i = 2; i <= m; ++ i) {
        LL val = std::min(abs(a[i].x - a[i - 1].x), abs(a[i].y - a[i - 1].y));
        AddEdge(a[i - 1].id, a[i].id, val);
        AddEdge(a[i].id, a[i - 1].id, val);
      }
      std::sort(a + 1, a + m + 1, CMPy);
      for (int i = 2; i <= m; ++ i) {
        LL val = std::min(abs(a[i].x - a[i - 1].x), abs(a[i].y - a[i - 1].y));
        AddEdge(a[i - 1].id, a[i].id, val);
        AddEdge(a[i].id, a[i - 1].id, val);
      }
      Dijkstra(0);
      printf("%lld
    ", dis[m + 1]);
      return 0;
    }
    
    作者@Luckyblock,转载请声明出处。
  • 相关阅读:
    linux--->PHP常用模块解析
    php--->php 缓冲区 buffer 原理
    php--->php打印格式化
    mysql--->MySQL错误日志
    mysql--->mysql慢查询
    单双引号问题
    博客园图片显示问题
    laravel 常用知识总结
    laravel config文件的使用
    laravel 接收json串
  • 原文地址:https://www.cnblogs.com/luckyblock/p/14077899.html
Copyright © 2011-2022 走看看