「THP3考前信心賽」題解



寫在前面

比賽地址:THP3 考前信心賽

感謝原出題人的貢獻:第一題 CF1422C,第四題 CF1422D。

所有題目背景均出自 《秘封俱樂部》系列專輯附帶故事,感謝太田順也先生的創造。

感謝 KersenxwmwrYpayRainycolor_Mahou 的鼎力相助。

超級親民的一場,大片部分分,沒有任何高級算法技巧,有一車大樣例。

A 未來宇宙

給定一長度為 \(n\) 的只由 \(0\sim 9\) 構成的字符串,求刪除一個任意非空子串后得到的所有十進制數 的和。
答案對 \(10^9 + 7\) 取模。
\(1\le n\le 2 \times 10^6\)
1S,128MB。

定義 \(f(l,r)\) 表示子串 \([l,r]\) 組成的十進制數。
考慮枚舉刪除的子串的最后一位 \(x\),得到的十進制數的和為:

\[\begin{aligned} &\sum_{i=1}^{x-1}{\left\{ 10^{n-x}\times f(1,i) +f(x+1,n)\right\}}\\ =& (x-1)\times f(x+1,n) + 10^{n-x}\times \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\n", ans);
  return 0;
}

B 空海澄澈

定義:

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

\(m\) 次詢問,每次詢問給定參數 \(l,r\),求:

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

\(1\le m\le 10^6\)\(1\le l \le r\le 10^6\)
1S,128MB。

這個式子是 LuckyblockP5518 [MtOI2019]幽靈樂團 的時候化出來的,因為比較基礎,所以就拿過來用了。

考慮化一下 \(f\)

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

考慮對於每一個 \(1\sim 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} d\sum_{i=1}^{n}[\gcd(i,n) =d] \]

考慮什么樣的 \(i\),滿足 \(\gcd(i,n) = d\),顯然當且僅當 \(i=kd(k\in \mathbb{N^*})\),且 \(\gcd(k,\frac{n}{d})=1\) 時滿足條件。為保證 \(i\le n\),有 \(k \le \left\lfloor\frac{n}{d}\right\rfloor\)
於是考慮把 \(d\) 提出來,改為枚舉上述的 \(k\),原式等於:

\[f(n) = \sum_{d|n} d \sum_{k=1}^{\left\lfloor\frac{n}{d}\right\rfloor}\left[\gcd\left(k,\frac{n}{d}\right)=1\right] \]

考慮后面一個 \(\sum\) 的實際含義,表示 \(1\sim \frac{n}{d}\) 中與 \(\frac{n}{d}\) 互質的數的個數,符合歐拉函數的定義,於是原式等於:

\[f(n) = \sum_{d|n} d \cdot \varphi\left(\frac{n}{d}\right) \]

線性篩預處理 \(\varphi\) 后,用埃氏篩即可篩出 \(1\sim 10^6\) 的所有 \(f\)
做個前綴和即可回答區間詢問。

復雜度 \(O(n\log 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\n", (sum[r] - sum[l - 1] + mod) % mod);
  }
  return 0;
}

C 舊約酒館

給定兩周長為 \(n\)\(01\) 環。
將它們疊放在一起,可以隨意旋轉兩個環。
定義一種放置方案的價值為兩個環對應位置的與的和,求最大價值。
\(1\le n\le 5\times 10^4\)
1S,128MB。

這題是今年 10 月份去刷題班的晚上偷着聽 mp3 的時候想出來的。
那個 mp3 就如題面所說,可以通過旋轉獲得不同的音量大小,實在是太厲害了= =

算法一

先把兩個環展開,固定其中一個環,枚舉另一個環的最多 \(n\) 種形態。
使用 bool 數組記錄形態,暴力與起來求答案,復雜度 \(O(n^2)\)

算法二

發現如果 \(n\) 較小的話,可以直接用整形變量存下展開后的環。
修改環的形態,可以在前一個形態的基礎上通過位運算簡單得到。
取與操作的復雜度也變得很小,總復雜度僅為 \(O(n)\) 級別。

考慮把一長度為 \(n\)bool 數組壓成一長度為 \(\frac{n}{64}\)unsigned long long 數組。
形態變化可以通過右移和賦值完成,取與時對每一個數分別取與,並求 \(1\) 的個數。
總復雜度 \(O\left(\dfrac{n^2}{64}\right)\)


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\n", ans);
  return 0;
}

D 博物之志

給定一 \(n\times n\) 的網格,圖中有 \(m\) 個給定的關鍵點。
給定人的起點終點,每次可以向上下左右任意方向移動一格。
特別地,當人與一個關鍵點橫坐標相同或縱坐標相同時,可以瞬移到關鍵點,不花費次數。
求從起點到終點的最小移動次數。
\(1\le n\le 10^9\)\(1\le m\le 10^5\)
1S,256MB。

算法一

有個顯然的暴力,每個點向上下左右的點連權值為 1 的雙向邊。每個關鍵點向同行同列的點連權值為 1 的雙向邊。然后跑 Dijkstra。
點數邊數是 \(O(n^2)\) 級別的,時間復雜度 \(O(n^2\log (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\n", 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\n", dis[Id(tx, ty)]);
  return 0;
}

算法二

\(n\) 這么大,顯然標算是個與 \(n\) 無關的算法。

考慮從起點到終點的最短路徑。
若不經過任何一個關鍵點,最短路即為兩點曼哈頓距離,可以直接算出。
否則可以把最短路看成:起點 \(\rightarrow\) 關鍵點 \(\rightarrow\) 終點。
於是將關鍵點作為中繼點,改變連邊方式:

  • 起點向關鍵點連邊,權值為 \(\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\n", dis[m + 1]);
  return 0;
}

算法三

為表達方便,以下欽定兩關鍵點間的距離為 \(\min(|x_1-x_2|, |y_1-y_2|)\)

考慮三個關鍵點之間的連邊,如果出現下圖情況:

顯然 \(A\rightarrow C\) 的距離不小於 \(A\rightarrow B\)\(B\rightarrow C\) 的距離之和。
因此可以不連 \(A\rightarrow C\) 的邊,不會影響 \(A\rightarrow C\) 的最短路,可以刪除這條邊。

再考慮更一般的情況,如果有下圖:

\(A\rightarrow C\) 的距離仍然不小於 \(A\rightarrow B\)\(B\rightarrow C\) 的距離之和。
因此可以不連 \(A\rightarrow C\) 的邊,不會影響 \(A\rightarrow C\) 的最短路。
但注意到 \(A\rightarrow C\) 的邊會對 \(A\rightarrow B\) 的最短路作出貢獻,這條邊不能刪除。

於是得到一個對算法二的優化:
先把關鍵點按 \(x\) 坐標排序,在排序后相鄰兩個點連 雙向邊。再把關鍵點按 \(y\) 坐標排序,在排序后相鄰兩點連 雙向邊
跑出來的最短路與之前的相等,但點數邊數僅為 \(O(m)\) 級別,時間復雜度 \(O(m\log 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\n", dis[m + 1]);
  return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM