Codeforces Round #675
写在前面
Codeforces Round #675 (Div. 2)
比赛地址:https://codeforces.com/contest/1422。
T2 写平均数被卡了。
这场的 C D 挺优秀,部分分也很好出,就当下一场 TouHouProblem 的候选题了。
A
\(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
\(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。
对于最后的矩阵,显然有:
把矩阵元素 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
给定一长度为 \(n\) 的只由 \(0\sim 9\) 构成的字符串,求删除任意非空子串后得到的十进制数的和。
\(1\le n\le 10^5\)。
定义 \(f(l,r)\) 表示子串 \([l,r]\) 组成的十进制数。
考虑枚举删除的子串的最后一位 \(x\),得到的十进制数的和为:
预处理出后缀表示的十进制数,枚举 \(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
给定一 \(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
给定一只由小写字母组成的字符串 \(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
给定一长度为 \(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\)。
算法一
根据小学奥数,有:
从 246. 区间最大公约数 - AcWing 学到区间 \(\gcd\) 可以根据辗转相减简单维护,再维护区间乘积即可。
但在实现时,发现取模会影响区间的合并,因此不能在运算过程中取模,于是炸 LL
了,期望得分 0pts。
算法二
首先对 \(a\) 质因数分解,设 \(a_i\) 分解后为 \(\prod\limits_j p_j^{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 就看数组大小。