场次链接:Educational Codeforces Round 9
这场还挺有意思的?可能大多都是图论题和一些经典的DS,导致补的很舒服吧()
A.Grandma Laura and Apples
题目大意:
有 n n n 个人按顺序买苹果,每次只买你手上剩下的一半,如果不能整除则免费多送一个苹果(即减一再除二)。每个苹果 p p p 元,送出的苹果不算价钱。最后你手上苹果卖完了,问你最后卖了多少钱。
解题思路:
直接模拟即可,记得开 l o n g long long l o n g long long。
时间复杂度: O ( n ) O(n) O(n)
AC代码:
void solve() {
int n, p;
cin >> n >> p;
vector<string> str(n);
for (auto& s : str) {
cin >> s;
}
i64 last = 0, del = 0;
while (str.size()) {
last <<= 1;
if (str.back().find("plus") != string::npos) {
++last;
del += p;
}
str.pop_back();
}
cout << last * p - del / 2 << '\n';
}
B.Alice, Bob, Two Teams
题目大意:
有 n n n 个棋子站在一排,只会是 A A A 和 B B B 两种,第 i i i 个位置上棋子的值为 a i a_i ai 。你可以选择前缀的一段区间或是后缀的一段区间,然后将 A A A 改成 B B B ,将 B B B 改成 A A A 。
问你最后为 B B B 的棋子的值之和最大为多少。
解题思路:
枚举一下前后缀的反转位置,取最优即可。
时间复杂度: O ( n ) O(n) O(n)
AC代码:
void solve() {
int n;
cin >> n;
vector<int> arr(n);
for (auto& v : arr) {
cin >> v;
}
string str;
cin >> str;
i64 sumA = 0, sumB = 0;
for (int i = 0; i < n; ++i) {
if (str[i] == 'A') sumA += arr[i];
else sumB += arr[i];
}
sumA = 0;
i64 A = 0, B = 0, ans = sumB;
for (int i = 0; i < n; ++i) {
if (str[i] == 'A') A += arr[i];
else B += arr[i];
ans = max(ans, sumB - B + A);
}
A = B = 0;
for (int i = n - 1; i >= 0; --i) {
if (str[i] == 'A') A += arr[i];
else B += arr[i];
ans = max(ans, sumB - B + A);
}
cout << ans << '\n';
}
C.The Smallest String Concatenation
题目大意:
给出 n n n 个字符串,你要根据某个顺序来拼接字符串,使得拼接得到的最终字符串字典序最小。
解题思路:
考虑一个问题,假设我只有字符串 S S S 和 T T T 。那么显然我只要查看 S + T S + T S+T 和 T + S T + S T+S 哪个的字典序最小即可。
扩展一下这个情况,假设我们有一个字符串数组 S S S 。
假设有 ( S 0 + S 1 < S 1 + S 0 ) (S_0+S_1<S_1+S_0) (S0+S1<S1+S0), ( S 1 + S 2 < S 2 + S 1 ) (S_1+S_2<S_2+S_1) (S1+S2<S2+S1)
那么 S 0 + S 1 + S 2 S_0+S_1+S_2 S0+S1+S2 一定是一个最小的字典序。我们根据 S i + S j < S j + S i S_i+S_j<S_j+S_i Si+Sj<Sj+Si 对整个字符串排序即可。
时间复杂度: O ( n log n ) O(n \log n) O(nlogn)
AC代码:
void solve() {
int n;
cin >> n;
vector<string> str(n);
for (auto& s : str) {
cin >> s;
}
sort(str.begin(), str.end(), [&](const string& s1, const string& s2) {
return s1 + s2 < s2 + s1;
});
for (auto& s : str) {
cout << s;
}
D.Longest Subsequence
题目大意:
给出一个长为 n n n 的数组和一个数字 m m m ,你要选出一些数字 a i a_i ai 组成一个新数组使得 l c m ( c 1 , c 2 , . . . , c k ) ≤ m lcm(c_1,c_2,...,c_k) \leq m lcm(c1,c2,...,ck)≤m ,问你组成的数组长度 k k k 最大为多少(其中 l c m ( a , b ) lcm(a,b) lcm(a,b) 代表所有数字 a , b a,b a,b 的最大公倍数)。
规定空数组的 l c m lcm lcm 为 1 1 1 。
解题思路:
容易发现 a i ≤ 1 0 9 a_i \leq 10^{9} ai≤109 的条件是没用的,因为 m ≤ 1 0 6 m \leq 10^{6} m≤106,我们只需要考虑 a i ≤ 1 0 6 a_i \leq 10^{6} ai≤106 的数字。
假设我们固定了 l c m lcm lcm 的值,那么整个数组中我们只能选出 l c m lcm lcm 的因子。否则它们的 l c m lcm lcm 值一定会和我们当前固定的 l c m lcm lcm 不同。
但是枚举 l c m lcm lcm 再去找 a i a_i ai 显然复杂度不对,我们可以换个角度考虑,只用考虑 a i a_i ai 作为因子能对哪个 l c m lcm lcm 有贡献即可。
先把所有 a i a_i ai 装桶里,然后对于每个 a i a_i ai 的倍数(一定会对这个倍数有贡献)加上该数字的贡献(数量)即可,时间复杂度为调和级数级别的。
时间复杂度: O ( m log m ) O(m \log m) O(mlogm)
AC代码:
void solve() {
int n, m;
cin >> n >> m;
vector<int> arr(n + 1);
vector<int> cnt(m + 1), ans(m + 1);
for (int i = 1; i <= n; ++i) {
cin >> arr[i];
if (arr[i] <= m) {
++cnt[arr[i]];
}
}
for (int i = 1; i <= m; ++i) {
for (int j = i; cnt[i] && j <= m; j += i) {
ans[j] += cnt[i];
}
}
int lcm = max<int>(1, max_element(ans.begin(), ans.end()) - ans.begin());
cout << lcm << " " << ans[lcm] << '\n';
for (int i = 1; i <= n; ++i) {
if (lcm % arr[i] == 0) {
cout << i << " ";
}
}
cout << '\n';
}
E.Thief in a Shop
题目大意:
你有一个长度为 n n n 的数组 a a a ,和一个数字 k k k,每个 a i a_i ai 的数量是无限的。你可以拿任意多个 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an 构成新的数组 c c c ,但数组 c c c 的长度必须恰好为 k k k,问你有多少个不同的 ∑ i = 1 k c i \sum_{i=1}^{k} c_i ∑i=1kci 能被凑成。
解题思路:
完全背包。
假设当前数组 a a a 已经排好了序,那么我们能够凑成的最小值一定是 k ⋅ a 1 k \cdot a_1 k⋅a1,没有更小的方法了。
设 d p [ i ] dp[i] dp[i] 为凑成 k ⋅ a 1 + i k \cdot a_1+i k⋅a1+i 所需要的非 a 1 a_1 a1 的数字的个数。
假设
d
p
[
i
]
=
0
dp[i] = 0
dp[i]=0 那么就是选了
k
k
k 个
a
1
a_1
a1 就能构成。
假设
d
p
[
i
]
=
1
dp[i] = 1
dp[i]=1 那么就是选了
(
k
−
1
)
(k-1)
(k−1) 个
a
1
a_1
a1 然后再选一个
a
j
a_j
aj 构成的。
那么我们只需要考虑把一些 a 1 a_1 a1 换成一些更大的数字即可。当然,我们可以把问题变得更简单,让所有 a 2 , a 3 , . . . , a n a_2,a_3,...,a_n a2,a3,...,an 都减去一个 a 1 a_1 a1,这样我们就只用考虑数字的增量。
接下来那么就是做一个完全背包就好了。
时间复杂度: O ( n k max ( a i ) ) O(nk\max( a_i)) O(nkmax(ai))
AC代码:
void solve() {
int n, k;
cin >> n >> k;
vector<int> arr(n);
for (auto& v : arr) {
cin >> v;
}
sort(arr.begin(), arr.end());
int mn = arr[0], mx = arr.back();
for (auto& v : arr) {
v -= mn;
}
int m = mx * k + 5;
vector<int> dp(m, 1e9);
dp[0] = 0;
mn *= k;
for (int i = 1; i < n; ++i) {
for (int j = arr[i]; j < m; ++j) {
dp[j] = min(dp[j], dp[j - arr[i]] + 1);
}
}
cout << '\n';
for (int i = 0; i < m; ++i) {
if (dp[i] <= k) {
cout << mn + i << " ";
}
}
}
F.Magic Matrix
题目大意:
给出一个 n × n n \times n n×n 的矩阵 A A A。
一个矩阵是魔法矩阵当且仅当:
- 主对角线上的元素为 0 0 0 ,即 a i , i = 0 a_{i,i}=0 ai,i=0 。
- 主对角线对称位置上的元素相等,即 a i , j = a j , i a_{i,j}=a_{j,i} ai,j=aj,i 。
- 存在一个 k k k,使得 1 ≤ k ≤ n 1 \leq k \leq n 1≤k≤n 且 a i , j ≤ max ( a i , k , a j , k ) a_{i,j} \leq \max(a_{i,k},a_{j,k}) ai,j≤max(ai,k,aj,k)
给出这个矩阵,你要判断当前矩阵是不是魔法矩阵。
解题思路:
有 b i t s e t bitset bitset 乱搞的做法,这里讲的是正解。
第一第二个条件很好判断,我们只考虑第三个情况。
对于这个条件 a i , j ≤ max ( a i , k , a j , k ) a_{i,j} \leq \max(a_{i,k},a_{j,k}) ai,j≤max(ai,k,aj,k) 我们仍然可以继续展开,我们发现 a j , k = a k , j ≤ max ( a k , l , a l , j ) a_{j,k}=a_{k,j} \leq \max(a_{k,l},a_{l,j}) aj,k=ak,j≤max(ak,l,al,j),那么带入就是 a i , j ≤ max ( a i , k , a k , l , a l , j ) a_{i,j} \leq \max(a_{i,k},a_{k,l},a_{l,j}) ai,j≤max(ai,k,ak,l,al,j) 同理,我们也可以继续展开,然后得到一长串东西取 max \max max 。
这样很难看出东西,我们固然可以把矩阵元素 a i , j a_{i,j} ai,j 看成一条 ( i , j ) (i,j) (i,j) 的无向边,那么题意就转化成了:
对于所有的不包括边 ( i , j ) (i,j) (i,j) 的 ( i → j ) (i \rightarrow j) (i→j) 路径而言(路径的全集),边 ( i , j ) (i,j) (i,j) 的权值 ≤ \leq ≤ 路径边权的最大值。
例如 a i , j ≤ max ( a i , k , a k , l , a l , j ) a_{i,j} \leq \max(a_{i,k},a_{k,l},a_{l,j}) ai,j≤max(ai,k,ak,l,al,j) 。
我们可以看成 ( i → k → l → j ) (i \rightarrow k \rightarrow l \rightarrow j) (i→k→l→j) 的路径,且要满足边权 a i , j ≤ max ( a i , k , a k , l , a l , j ) a_{i,j} \leq \max(a_{i,k},a_{k,l},a_{l,j}) ai,j≤max(ai,k,ak,l,al,j) 。
注意到这是个经典的最小瓶颈路问题:
最小瓶颈路 问题是指在一张无向图中,询问点对 ( u , v ) (u,v) (u,v) 的一条简单路径,需要找出从 u u u 到 v v v 的一条简单路径,使路径上所有边中最大值最小。其中无向图最小生成树中从 u u u 到 v v v 的路径一定是 u u u 到 v v v 的最小瓶颈路之一(因为最小生成树可能不唯一)。
那么问题就转化成了,只用考虑 ( i → j ) (i \rightarrow j) (i→j) 路径上的瓶颈边即可。
可以用 k r u s k a l kruskal kruskal 重构树之类的方法来做,但其实我们可以在跑 k r u s k a l kruskal kruskal 的同时直接计算出来。
考虑 a i , j a_{i,j} ai,j 这条边是不是 ( i → j ) (i \rightarrow j) (i→j) 的路径上的最大值。显然,所有小于等于 a i , j a_{i,j} ai,j 的边都无影响,我们跑最小生成树时直接合并严格小于 a i , j a_{i,j} ai,j 的边即可(先往下看)。
那什么时候我们才会比路径上的所有边都要大呢?
如果出现: ( i → j ) (i \rightarrow j) (i→j) 只需要依靠比 a i , j a_{i,j} ai,j 权值严格小于的边,那么 a i , j a_{i,j} ai,j 一定大于这条最小瓶颈路上的所有边。
对所有边 ( i , j ) (i,j) (i,j) 判断一下 i , j i,j i,j 的可达性即可,注意不能同时合并所有权值相等的边(会影响并查集判断)。
当然,可以用 P r i m Prim Prim 将时间复杂度优化至 O ( n 2 ) O(n^2) O(n2),但个人认为 k r u s k a l kruskal kruskal 做法更容易理解一些。
时间复杂度: O ( n 2 log n 2 ) O(n^{2} \log n^{2}) O(n2logn2)
AC代码:
struct DSU {
vector<int> fa, siz;
DSU(int n) : fa(n + 1), siz(n + 1, 1) { iota(fa.begin(), fa.end(), 0); };
int find(int x) {
while (x != fa[x]) x = fa[x] = fa[fa[x]];
return x;
}
int size(int x) { return siz[find(x)]; }
bool same(int x, int y) { return find(x) == find(y); }
bool merge(int x, int y) {
x = find(x), y = find(y);
if (x == y) return false;
siz[y] += siz[x];
fa[x] = y;
return true;
}
};
struct Edge {
int u, v, w;
Edge() : Edge(0, 0, 0) {}
Edge(int u, int v, int w) : u(u), v(v), w(w) {}
bool operator<(const Edge& rhs) const {
return w < rhs.w;
}
};
const int N = 2500;
int g[N][N];
void solve() {
int n;
cin >> n;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
cin >> g[i][j];
}
}
vector<Edge> edge;
for (int i = 0; i < n; ++i) {
if (g[i][i]) {
cout << "NOT MAGIC\n";
return;
}
for (int j = i + 1; j < n; ++j) {
if (g[i][j] != g[j][i]) {
cout << "NOT MAGIC\n";
return;
}
edge.emplace_back(i, j, g[i][j]);
}
}
sort(edge.begin(), edge.end());
DSU dsu(n);
int last = -1;
vector<PII> hav;
for (auto& [u, v, w] : edge) {
if (w != last) {
last = w;
while (hav.size()) {
auto [x, y] = hav.back();
dsu.merge(x, y);
hav.pop_back();
}
}
if (dsu.same(u, v)) {
cout << "NOT MAGIC\n";
return;
}
hav.emplace_back(u, v);
}
cout << "MAGIC\n";
}