Codeforces Round #675


写在前面

Codeforces Round #675 (Div. 2)

比赛地址:https://codeforces.com/contest/1422

T2 写平均数被卡了。
这场的 C D 挺优秀,部分分也很好出,就当下一场 TouHouProblem 的候选题了。

A

Link

\(t\) 组数据,每次给定正整数 \(a,b,c\),求一个正整数 \(d\),满足存在一个四边形的四条边长分别为 \(a,b,c,d\)
\(1\le t\le 10^3\)\(1\le a,b,c\le 10^9\)
1S,256MB,SPJ。

输出 \(a+b+c-1\)

B

Link

\(t\) 组数据,每次给定一 \(n\times m\) 的矩阵 \(a\)
每次操作可以使任意位置 +1/-1,求至少进行多少次操作,使得矩阵的每一行,每一列都是回文的。
\(1\le t\le 10\)\(1\le n,m\le 100\)\(0\le a_{i,j}\le 10^9\)
1S,256MB。

对于最后的矩阵,显然有:

\[a_{x,y} = a_{x,m-y+1}=a_{n-x+1,y}=a_{n-x+1,m-y+1} \]

把矩阵元素 4 个一组拿出来看,显然这 4 个元素应调整为它们的中位数,平均数的话可能由于下取整出错。
注意特判奇数行/列的情况。

//知识点:瞎搞
/*
By:Luckyblock
*/
#include <cctype>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define LL long long
const int kN = 100 + 10;
//=============================================================
int n, m, a[kN][kN], tmp[5];
LL ans;
//=============================================================
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 Solve(int x_, int y_) {
  int cnt = 2 + 2 * (x_ != y_);
  for (int i = 1, j = m; i <= j; ++ i, -- j) {
    if (i == j) {
      if (x_ == y_) return ;
      cnt = 2;
      tmp[1] = a[x_][i];
      tmp[2] = a[y_][i];
    } else {
      tmp[1] = a[x_][i];
      tmp[2] = a[x_][j];
      tmp[3] = a[y_][i];
      tmp[4] = a[y_][j];
    }
    std::sort(tmp + 1, tmp + cnt + 1);
    for (int k = 1; k <= cnt; ++ k) {
      ans += 1ll * abs(tmp[k] - tmp[cnt / 2]);
    }
  }
}
//=============================================================
int main() {
  int t = read();
  while (t --) {
    n = read(), m = read();
    ans = 0;
    for (int i = 1; i <= n; ++ i) {
      for (int j = 1; j <= m; ++ j) {
        a[i][j] = read();
      }
    }
    for (int i = 1, j = n; i <= j; ++ i, -- j) {
      Solve(i, j);
    }
    printf("%lld\n", ans);
  }
  return 0;
}
/*
1
3 3 
1 2 3
4 5 6
7 8 9
*/

C

Link

给定一长度为 \(n\) 的只由 \(0\sim 9\) 构成的字符串,求删除任意非空子串后得到的十进制数的和。
\(1\le n\le 10^5\)

定义 \(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 = 1e5 + 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;
}

D

Link

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

算法一

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

算法二

考虑从起点到终点的最短路径。
若不经过任何一个关键点,最短路即为两点曼哈顿距离,可以直接算出。
否则可以把最短路看成:起点 \(\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))\) 级别,还是菜。

算法三

为表达方便,以下钦定两关键点间的距离为 \(\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;
}

E

Link

给定一只由小写字母组成的字符串 \(s\),可以先删除任意个 两相邻相同字符,再把剩下的部分拼接起来。
对于 \(s\) 的每个后缀,求进行上述操作后得到的字典序最小的字符串。
\(1\le |s|\le 10^5\)

发现是先删除再拼接,满足无后效性。考虑倒序枚举字符串 DP。
\(f_i\) 表示后缀 \([i,|s|]\) 操作后得到的字典序最小的字符串。
初始化 \(f_{|s|} = s_{|s|}\)
转移时考虑当前字符能否与上个字符相等,分类讨论:

  • \(s_i \not= s_{i+1}\),则 \(f_{i} = f_{i+1}\)
  • \(s_{i}=s_{i+1}\),且 \(s_i < f_{i+2,0}\),则接上该字符后字典序会变小,则 \(f_{i} = s_{i} + s_{i+1} + f_{i+2}\)
  • \(s_{i}=s_{i+1}\),且 \(s_{i}=f_{i+2,0}\),出现了连续相等的情况。此时需要比较 \(s_i\)\(f_{i+2}\) 从头数第 2 种字符 与 第一种字符(即 \(s_i\))的大小关系。
    若第二种字符 \(>s_i\),显然应该接上,则 \(f_{i} = s_{i} + s_{i+1} + f_{i+2}\)
    否则应该删去,有 \(f_{i} = f_{i + 2}\)

实现时用个结构体存一下 \(f\) 的前后缀,便于输出。
复杂度 \(O(n)\)

//知识点:贪心,DP
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
std::string s;
//=============================================================
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;
}
struct Suffix {
  std::string pre, suf;
  int ans, cmp;
  Suffix() {
    pre = suf = "";
    cmp = false;
  }
  void Insert(char ch_) {
    ++ ans;
    if (pre.size() && ch_ != pre[0]) cmp = (ch_ < pre[0]);
    if (suf.size() < 2) suf = ch_ + suf;
    pre = ch_ + pre;
    while (pre.size() > 10) pre.pop_back();
  }
} f[kN];
//=============================================================
int main() {
  std::cin >> s;
  int n = s.length() - 1;
  f[n].Insert(s[n]);
  for (int i = n - 1; i >= 0; -- i) {
    if (s[i] != s[i + 1]) {
      f[i] = f[i + 1];
      f[i].Insert(s[i]);
      continue ;
    }
    f[i] = f[i + 2];
    if (f[i].ans) {
      if (s[i] > f[i].pre[0]) continue ;
      if (s[i] == f[i].pre[0] && !f[i].cmp) continue ;
      f[i].Insert(s[i]), f[i].Insert(s[i]);
    }
  }
  for (int i = 0; i <= n; ++ i) {
    printf("%d ", f[i].ans);
    if (f[i].ans <= 10) {
      std::cout << f[i].pre << "\n";
    } else {
      std::cout << f[i].pre.substr(0, 5) << "..." << f[i].suf << "\n";
    }
  }
  return 0;
}

F

Link

给定一长度为 \(n\) 的序列 \(a\),有 \(q\) 次询问。
每次询问给定区间 \([l,r]\),求 \(\operatorname{lcm}(a_l, \cdots,a_{r}) \bmod 10^9 + 7\)
强制在线。
\(1\le n,q\le 10^5\)\(1\le a_i\le 2\times 10^5\)

算法一

根据小学奥数,有:

\[\large \operatorname{lcm}(a_l, \cdots,a_{r}) = \frac{\prod\limits_{i=l}^{r} a_i}{\gcd(a_l,\cdots,a_r)} \]

246. 区间最大公约数 - AcWing 学到区间 \(\gcd\) 可以根据辗转相减简单维护,再维护区间乘积即可。
但在实现时,发现取模会影响区间的合并,因此不能在运算过程中取模,于是炸 LL 了,期望得分 0pts。

算法二

首先对 \(a\) 质因数分解,设 \(a_i\) 分解后为 \(\prod\limits_j p_j^{c_{i,j}}\)

根据小学奥数,有:

\[\large \operatorname{lcm}(a_l, \cdots,a_{r}) = \prod\limits_j p_j^{\max\limits_{l\le i\le r}(c_{i,j})} \]

如果不强制在线,用莫队维护下各质因子出现次数即可。
但恶心出题人强制在线,期望得分 0pts。

算法三

发现算法二中有区间取 \(\max\) 的操作,考虑对每个质因子都建立一棵线段树,维护区间该质因子次数的最大值。查询时枚举质因子,累乘贡献即可。
时空复杂度 \(O(kn\log n)\)\(k\) 为质因子个数,可知 \(k \approx \frac{n}{\ln n}\),时间空间均无法承受,期望得分 0pts。

算法四

发现数的值域比较小,又出现了质因子这样的字眼,考虑套路根号分治。
先将不大于 \(\sqrt{2\times 10^5}\) 的质因子筛出,发现仅有 92 个,于是仅建立 92 棵线段树,套用算法三,维护区间该质因子次数最大值。

再考虑大于 \(\sqrt{2\times 10^5}\) 的质因子,它们在每个 \(a_i\) 中的次数最多为 1,取 \(\max\) 后次数也只能为 1。
则每种质因子只能贡献一次,查询区间内它们的贡献,即为查询区间内 不同权值 的乘积。
这是个经典问题,可以看这里统计区间里有多少个不同的数__莫队__主席树__树状数组_dctika1584的博客。不强制在线可以用线段树 + 扫描线,通过单点修改维护。强制在线,套路地可持久化即可。

总时空间复杂度均为 \(O(kn\log n + n\log^2 n)\)\(k=92\),能过。

对于不大于 \(\sqrt{2\times 10^5}\) 的质因子,质因子的幂 \(\le 2\times 10^5\),因此代码中直接维护了该质因子的幂的最大值,而不是上述的次数。

//知识点:线段树,主席树 
/*
By:Luckyblock
注意在维护较小质因子的线段树中不能取模
合并的时候写的乘,并非取最大值。
我是,傻逼,哈哈。
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
const int kMax = 2e5;
const int mod = 1e9 + 7;
//=============================================================
int p_num, p[kMax + 10];
bool vis[kMax + 10];
int n, m, ans, a[kN], last[kN], lastv[kMax];
int root[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 Euler() {
  for (int i = 2; i <= kMax; ++ i) {
    if (! vis[i]) p[++ p_num] = i;
    for (int j = 1; j <= p_num && i * p[j] <= kMax; ++ j) {
      vis[i * p[j]] = true;
      if (i % p[j] == 0) break;
    }
  }
}
int QPow(int x_, int y_) {
  x_ %= mod;
  int ret = 1;
  for (; y_; y_ >>= 1) {
    if (y_ & 1) ret = 1ll * ret * x_ % mod;
    x_ = 1ll * x_ * x_ % mod;
  }
  return ret;
}
int Inv(int x_) {
  return QPow(x_, mod - 2);
}
namespace Hjt {
  #define ls (lson[now_])
  #define rs (rson[now_])
  #define mid ((L_+R_)>>1)
  int node_num, lson[kN << 6], rson[kN << 6], t[kN << 6];
  void Init() {
    t[0] = 1;
  }
  void Insert(int &now_, int pre_, int L_, int R_, int pos_, int val_) {
    now_ = ++ node_num;
    t[now_] = 1ll * t[pre_] * val_ % mod;
    if (L_ == R_) return ;
    ls = lson[pre_], rs = rson[pre_];
    if (pos_ <= mid) Insert(ls, lson[pre_], L_, mid, pos_, val_);
    else Insert(rs, rson[pre_], mid + 1, R_, pos_, val_);
  }
  int Query(int now_, int L_, int R_, int l_, int r_) {
    if (l_ <= L_ && R_ <= r_) return t[now_];
    int ret = 1;
    if (l_ <= mid) ret = 1ll * ret * Query(ls, L_, mid, l_, r_) % mod;
    if (r_ > mid) ret = 1ll * ret * Query(rs, mid + 1, R_, l_, r_) % mod;
    return ret;
  }
  #undef ls
  #undef rs
  #undef mid
}
#define ls (now_<<1)
#define rs (now_<<1|1)
#define mid ((L_+R_)>>1)
struct Seg {
  int t[kN << 2];
  void Build(int now_, int L_, int R_) {
    t[now_] = 1;
    if (L_ == R_) return ;
    Build(ls, L_, mid);
    Build(rs, mid +1, R_);
  }
  void Pushup(int now_) {
    t[now_] = std::max(t[ls], t[rs]);
  }
  void Insert(int now_, int L_, int R_, int pos_, int val_) {
    if (L_ == R_) {
      t[now_] = val_;
      return ;
    }
    if (pos_ <= mid) Insert(ls, L_, mid, pos_, val_);
    else Insert(rs, mid + 1, R_, pos_, val_);
    Pushup(now_);
  }
  int Query(int now_, int L_, int R_, int l_, int r_) {
    if (l_ <= L_ && R_ <= r_) return t[now_]; 
    int ret = 1;
    if (l_ <= mid) Chkmax(ret, Query(ls, L_, mid, l_, r_));
    if (r_ > mid) Chkmax(ret, Query(rs, mid + 1, R_, l_, r_));
    return ret;
  }
} t[93];
#undef ls
#undef rs
#undef mid
void Insert(int pos_) {
  int val = a[pos_] = read();
  for (int i = 1; p[i] * p[i] <= kMax; ++ i) {
    int d = p[i], delta = 1;
    if (val == 1) break;
    if (val % d) continue ;
    while (val % d == 0) {
      val /= d;
      delta *= d;
    }
    t[i].Insert(1, 1, n, pos_, delta);
  }
  root[pos_] = root[pos_ - 1];
  Hjt::Insert(root[pos_], root[pos_], 1, n, pos_, val);
  if (lastv[val]) Hjt::Insert(root[pos_], root[pos_], 1, n, lastv[val], Inv(val));
  lastv[val] = pos_;
}
int Query(int l_, int r_) {
  int ret = 1;
  for (int i = 1; p[i] * p[i] <= kMax; ++ i) {
    ret = 1ll * ret * std::max(1, t[i].Query(1, 1, n, l_, r_)) % mod;
  }
  ret = 1ll * ret * Hjt::Query(root[r_], 1, n, l_, r_) % mod;
  return ret;
}
void Prepare() {
  Euler();
  Hjt::Init();
  n = read();
  for (int i = 1; i <= n; ++ i) Insert(i);
}
//=============================================================
int main() {
  Prepare();
  m = read();
  while (m --) {
    int l = (read() + ans) % n + 1, r = (read() + ans) % n + 1;
    if (l > r) std::swap(l, r);
    printf("%d\n", ans = Query(l, r));
  }
  return 0;
}

总结

  • 注意平均数的下取整。
  • 强制在线题 RE 可能是因为某次答案变成负数导致,不要一 RE 就看数组大小。
posted @ 2020-11-15 22:40  Luckyblock  阅读(89)  评论(1编辑  收藏  举报