比赛速览
本场比赛总体难度较小
- A. 数学小技巧
- B. 概率基础知识
- C. 逆向思维(致谢:高同学)
- D. DFS爆搜(也有人才用轮廓线DP,真没必要)
- E. 括号模型,思维题(代码值得一看)
- F. 线段树优化/两次前缀和优化(本题值得积累)
- G. 网格问题转二分图转费用流
A - Approximation
给 A A A和 B B B,求距离 A B \frac{A}{B} BA最近的整数。
先直接计算出下取整的结果 X X X,比较 X X X与 X + 1 X+1 X+1谁距离 A B \frac{A}{B} BA 更近。
但是有更简便的方式,直接使用printf四舍五入。
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a, b;
cin >> a >> b;
double t = a * 1.0 / b;
printf("%.0lf\n", t); // 人才
return 0;
}
B - P(X or Y)
骰两个骰子的到 A A A和 B B B点,计算如下两条规则至少一个满足的概率。
- A A A 与 B B B的和 > = X >= X >=X
- A A A 与 B B B的差的绝对值 > = Y >= Y >=Y
本题输出要保留至少 10 10 10位小数。
两层循环枚举 A A A和 B B B,统计至少满足两种条件之一的情况有多少种。除以 36 36 36即可。
#include <bits/stdc++.h>
using namespace std;
int main() {
int X, Y, c = 0;
cin >> X >> Y;
for (int i = 1; i <= 6; ++i)
for (int j = 1; j <= 6; ++j)
c += X <= i + j || Y <= abs(i - j);
cout << fixed << setprecision(10);
cout << (double)c / 36 << endl;
}
C - Security 2
致谢:高同学提供C题逆向解题思路
给定一个字符串 S S S,请你将初始位空串的 T T T变换成 S S S。你有两种操作可以任意操作:
- A A A操作可以在 T T T的结尾补充一个 0 0 0
- B B B操作可以让 T T T的每一位都 + 1 , 9 + 1 +1, 9+1 +1,9+1 变成 0 0 0
求最少的操作次数
1 ≤ ∣ S ∣ ≤ 5 × 10 5 1\leq |S| \leq 5\times 10^5 1≤∣S∣≤5×105
从数据量上一眼可以看出这应该是个策略贪心。本题实际上只有唯一解,不存在最小解。唯一解就是最小解。题目问最小解实际上是障眼法。
方法一 正向思维
- 考虑 B B B操作最特殊,因为 B B B操作是一个全局影响的操作, A A A操作只是一个局部影响。
-
B
B
B操作在执行的时候,不会改变数字之间的相对距离。例如原本是
274
, 操作B之后变成385
, 但是三个数字之间的相对差值不变。所以这是入手点。 - 实际上只要保持 T T T的相邻两个数字之间的相对大小 与 S S S的对应位置相对大小一致就可以最后通过一些 B B B操作使得 T T T变成 S S S。
- 那么现在只剩下一件事,就是如何保证第 2 2 2个数字与第 1 1 1个数字能够保持 S S S的模式。
例如:
S
S
S的前两位是27
:
- 先做
A
B
B
ABB
ABB可以让
T
T
T变成
2
- 然后如果再补充新数字进来只能补充 0 0 0,无法满足相对值不变。这里需要逆向思维一下。我们知道第二个数字补进来一定是 0 0 0,所以我们先计算出第一位如果保持相对大小不变应该是: 5 5 5。 所以应该先把前面的数字搞成 5 5 5,再补 0 0 0。 而不是先搞成 2 2 2。
方法二 逆向思维(来自高同学)
考虑逆向操作,把 S S S变成一个空串。那么 B B B操作就是整体 − 1 -1 −1,然后依次把最后一位数变成 0 0 0, 然后丢掉。记录下为了把最后一位变成 0 0 0一共做了多少次 B B B,就可以计算出当前位此时是多少了。
#include<bits/stdc++.h>
using namespace std;
int main(){
string s;
cin>>s;
int ans=0;
int res=0;//减了多少
for(int i=s.size()-1;i>=0;i--){
int t=s[i]-'0';
t=(((t-res)%10)+10)%10;
ans+=t+1;
res+=t;
}
cout<<ans;
}
D - Domino Covering XOR
给定一个 H H H行 W W W列的数阵 A A A。你可以在数阵中放置 1 × 2 1\times 2 1×2的多米诺骨牌,骨牌不可以交叠,不可以出界。一种放置方案的得分计算规则: 所有未被覆盖的数字的异或和。
- 1 ≤ H W ≤ 20 1 \leq HW \leq 20 1≤HW≤20
从复杂度看就是一眼水题,一共 20 20 20个位置,枚举每个位置是否被覆盖(先别管骨牌), 然后我们看这个覆盖方案是否合法,即能否找到一个骨牌覆盖出这种模式。剩下的就是暴力计算得分了。
难点在于如何判断覆盖是否合法。那么这个问题本身就有点困难,是一个轮廓线 D P DP DP的问题。
换个枚举姿势:每个位置是否放骨牌、放横的骨牌、放竖的骨牌。采用 D F S DFS DFS的写法,每次放骨牌就 v i s vis vis标记上覆盖的位置,放的时候也要判断是否会出现交叠和出界。有了这些判断就可以大大缩小复杂度了,实际复杂度不到 3 20 3^{20} 320。
#include <bits/stdc++.h>
using namespace std;
#define int long long
int n, m;
int a[22];
int ans = 0;
bool vis[22];
int id(int x, int y) {
return x * m + y;
}
void dfs(int x) {
if (x >= n*m) {
int res = 0;
for (int i = 0; i < n*m; i++) {
if (!vis[i]) res ^= a[i];
}
ans = max(ans, res);
return;
}
dfs(x+1);
if (vis[x]) return;
int realx = x / m, realy = x % m;
if (realx+1 < n && !vis[x+m]) {
vis[x] = 1;
vis[x+m] = 1;
dfs(x+1);
vis[x] = 0;
vis[x+m] = 0;
}
if (realy+1 < m && !vis[x+1]) {
vis[x] = 1;
vis[x+1] = 1;
dfs(x+1);
vis[x] = 0;
vis[x+1] = 0;
}
}
signed main() {
cin >> n >> m;
for (int i = 0; i < n*m; i++) cin >> a[i];
dfs(0);
cout << ans;
}
E - Most Valuable Parentheses
给定一个长度为 2 N 2N 2N的序列 A A A,请你构造一个合法的括号序列 s s s,括号序列的评分规则:所有左括号
(
位置 i i i对应 A i A_i Ai的总和请你计算最大的得分会是多少。
多组测试样例, 1 ≤ T ≤ 500 1\leq T\leq 500 1≤T≤500
1 ≤ N ≤ 2 × 10 5 1 \leq N \leq 2\times 10^5 1≤N≤2×105
∑ N ≤ 2 × 10 5 \sum N \leq 2\times 10^5 ∑N≤2×105
从简单的贪心策略上我们肯定希望保留最大的
N
N
N个数字放(
, 最小的
N
N
N个数字放)
。但是由于前缀合法性的要求,如果前缀中的)
过多了,就必须先放(
进来。
例如:(()))
对于这个情况,前缀出现的
3
3
3个)
一定有一个要变成(
, 那么一定选取最大的一个数字来变。所以我们需要维护前缀中的)
, 并按照数值从大到小,每次能够取出最大的一个做替换。所以需要优先队列。
综上,我们预先给最大的
N
N
N个数字分配(
, 然后从头开始扫描,遇到(
不够的时候要选择一个最大的)
来变成(
, 直到分配完
N
N
N个(
就可以了。
这位同学的代码非常简洁, 可以参考一下,思路是一样的,实现很简洁。
#include <bits/stdc++.h>
using namespace std;
priority_queue<int> a;
int main (){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int T;
cin >> T;
for (;T--;){
int n;long long cnt = 0;
cin >> n;
n <<= 1;
for (int i = 1;i <= n;i++){
int x;
cin >> x;
a.push(x);
if (i & 1) cnt += a.top(),a.pop();
}cout << cnt << '\n';a = priority_queue<int>();
}
}
F - Sums of Sliding Window Maximum
给定一个长度为 N N N的序列 A A A, 对于所有的长度 K ( 1 ≤ K ≤ N ) K (1\leq K \leq N) K(1≤K≤N), 求出所有长度为 K K K的子段的 M a x Max Max,并加起来。输出对于每个 K K K的结果。
1 ≤ N ≤ 2 × 10 5 1\leq N \leq 2\times 10^5 1≤N≤2×105
水题 + 1 +1 +1
一眼就是贡献问题,算每个 A i A_i Ai 会贡献给哪些 K K K。
对于全局最大值 A i A_i Ai来说,只要包含自己的段,自己都是 M a x Max Max,都有贡献:
- 对于长度 K = 1 K=1 K=1的子段, A i A_i Ai只贡献 1 1 1次,就是它自己。
- 对于长度 K = 2 K=2 K=2的子段, A i A_i Ai贡献了 2 2 2次,就是 [ A i − 1 , A i ] [A_{i-1}, A_i] [Ai−1,Ai] 和 [ A i , A i + 1 ] [A_i, A_{i+1}] [Ai,Ai+1]
- 对于长度 K = 3 K=3 K=3的子段, A i A_i Ai贡献了 3 3 3次。
- 以此类推。
但是对于全局第 2 2 2大的数 A j A_j Aj来说,就不一样了,他对于每个长度的贡献要考虑最大值 A i A_i Ai。
如图:
- 对于长度 K = 1 , A j K=1, A_j K=1,Aj贡献 1 1 1次,
- 对于长度 K = 2 , A j K=2, A_j K=2,Aj贡献 2 2 2次,
- . . . ... ...
- 对于长度 K = 6 , A j K=6,A_j K=6,Aj 贡献 贡献了 6 6 6次。
- 对于长度 K = 7 , A j K=7,A_j K=7,Aj 贡献了 6 6 6次。
- 对于长度 K = 8 , A j K=8,A_j K=8,Aj 贡献了 6 6 6次。
- . . . ... ...
- 对于长度 K = 10 , A j K=10, A_j K=10,Aj 贡献了 6 6 6次。
- 对于长度 K = 11 , A j K=11, A_j K=11,Aj 贡献了 5 5 5次。
一般的,对于一个数字 A i A_i Ai, 其左侧最接近的大于他的数字是 A p A_p Ap, 右侧最接近的大于他的数字是 A q A_q Aq, 那么 A i A_i Ai的贡献规律是:
- Case 1. 对于 K ≤ M i n ( i − p , q − i ) K\leq Min(i-p, q-i) K≤Min(i−p,q−i), 贡献 K K K次。
- Case 2. 对于 M i n ( i − p , q − i ) < K < M a x ( i − p , q − i ) Min(i-p, q-i)< K < Max(i-p,q-i) Min(i−p,q−i)<K<Max(i−p,q−i), 贡献 M i n ( i − p , q − i ) Min(i-p, q-i) Min(i−p,q−i)次
- Case 3. 对于 M a x ( i − p , q − i ) < K < ( q − p ) Max(i-p, q-i) < K < (q-p) Max(i−p,q−i)<K<(q−p), 贡献 q − p − K q-p-K q−p−K次, 我们可以拆解为贡献 q − p q-p q−p次,再去掉 K K K次。
代码方法一
这是一个三段式的贡献,并且贡献的值与对象K相关。所以我们需要维护3棵线段树。
- 线段树
T
1
T1
T1维护
Case 2
的贡献值,和Case 3
中 q − p q-p q−p部分,区间加值 M i n ( i − p , q − i ) × A i Min(i-p,q-i)\times A_i Min(i−p,q−i)×Ai。 - 线段树
T
2
T2
T2维护
Case 1
中的 + K +K +K,对应的操作是区间 + A i +A_i +Ai 和区间 − A i -A_i −Ai, 并对区间结尾后一个位置的单点反向操作。 - 线段树
T
3
T3
T3维护
Case 3
中的 − K -K −K部分, 对应的操作是区间 − A i -A_i −Ai, 并对区间开头的前一个位置的单点反向操作。
最后对于一个K的贡献总和则是(T2的前缀和) + (T3的后缀和) + (T1的单点和)
如果不是很理解T2, 可以考虑这样一个问题:
给定一个序列 A A A,每次给区间 [ L , R ] [L, R] [L,R]中的每个位置 i i i都加上 ( i − L + 1 ) ∗ x i (i-L+1) * x_i (i−L+1)∗xi, 最后求每个位置的值。
注意这里的意思是,给 A L A_L AL 加 1 1 1次 x i x_i xi, 给 A L + 1 A_{L+1} AL+1加 2 2 2次 x i x_i xi, 以此类推。
由于此处区间加值并非是区间统一加同一个值,因此无法直接维护这个值到节点里。
但是我们可以在最后用前缀和的方式来累加这个值,得到最终的结果。即我们给 [ L , R ] [L, R] [L,R]的每个位置都只加 1 1 1个 x i x_i xi, 那么最终的 A L A_L AL就是前 L L L项和,也就只有 1 1 1个 x i , A L + 1 x_i,A_{L+1} xi,AL+1 则会把 A L A_L AL上加的 1 1 1个 x i x_i xi累加到自己身上,最终获得 2 2 2个 x i x_i xi, 以此类推。
对于 R + 1 R+1 R+1位置我们需要加一个抵消值,用来阻止这一段的 x i x_i xi累加到后面不需要的位置里。因此我们需要单点对 A R + 1 A_{R+1} AR+1减去一个 ( R − L + 1 ) ∗ x i (R-L+1) * x_i (R−L+1)∗xi。
同理考虑T3即可。
代码方法二
对于这种递增次数的贡献方式,我们也可以不用线段树。《算法导论》中就讲过这个知识,如果我们对一个序列做1次前缀和,前面的信息会同步一份给到后面,当我们在第一次基础上再做一次前缀和的时候,前面的信息会递增的传递到后面,即实现了本题的贡献方式。
方法二的代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int MAXN = 2e5 + 25;
int n, a[MAXN];
int L[MAXN], R[MAXN];
ll ans[MAXN];
vector<int>st;
void get_pre() {
for(int i = 1; i <= n; i++) {
ans[i] += ans[i - 1];
}
}
void solve() {
cin >> n;
st.clear();
for(int i = 1; i <= n; i++) {
cin >> a[i];
R[i] = n + 1;
L[i] = 0;
ans[i] = 0;
}
for(int i = 1; i <= n; i++) {
for(; st.size() && a[st.back()] <= a[i]; st.pop_back()) {
R[st.back()] = i;
}
if(st.size())L[i] = st.back();
st.push_back(i);
}
for(int i = 1; i <= n; i++) {
int l = i - L[i], r = R[i] - i;
if(l > r)swap(l, r);
ans[1] += a[i], ans[l + 1] -= a[i], ans[r + 1] -= a[i], ans[l + r + 1] += a[i];
}
// 两次前缀和。
get_pre();
get_pre();
for(int i = 1; i <= n; i++) {
cout << ans[i] << '\n';
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
int T = 1;
for(/*cin >> T*/; T--; solve());
return 0;
}
G - Domino Covering SUM
题面背景与D题一致,差异是:
- 数据有正负数,且记分算法变成了求和。
- 矩阵的大小变成了 1 ≤ H W ≤ 2000 1\leq HW \leq 2000 1≤HW≤2000
本题需要转为图结构来解决。
如果我们将格点看作是点,将相邻看作是连边。则这个图以相邻关系绘制出图会是一个二部图。(自行证明,可以黑白染色,也可以反证法)
定义边权为两点上数字之和。则原问题最大化剩余点的权值之和,等价于最小化边集。但是我们不知道要选多少条边,但是我们可以枚举选多少条边。
考虑另外一个问题:
对于一个正边权二分图来说,寻找最小的K匹配问题。即只选择K个匹配,保证K个匹配的边权之和最小。
这个问题我们可以使用最小费用最大流来解决。
本题只要先把所有边权都加 10 12 10^{12} 1012,就变成了上面全正权边的问题,最后再统一减掉就可以了。