比赛名称: 2021牛客暑期多校训练营1
A. Alice and Bob
大致题意
有两堆石子, 一堆有n个, 另外一堆有m个. 现在Alice 和 Bob轮流从两堆石子中取石子.
取法: 选择一堆石子拿走 k ( k > 0 ) k(k > 0) k(k>0)个, 另外一堆可以拿走 s × k ( s > = 0 ) s \times k (s >= 0) s×k(s>=0)个.
问: 如果Alice先手, 最先不能拿石子的是谁?
题目考查
博弈论 暴力SG 难度: Middle
解题思路
下文称Alice最终不能拿石子的状态为必败态
我们在玩博弈论的过程中较容易发现一个事实: 必败态很少, 几乎都是必胜态. 且若令 c = ∣ n − m ∣ c = |n - m| c=∣n−m∣, 则每一个c值至多对应一种必败态.
我们可以进行如下证明: 如果此时(n, m)为必败态, 则易得(n + x, m + x)为必胜态(其中满足x > 0, 对下文仍谈成立.)
对于(n - x, m - x)的情况一定也为必胜态.
Why? 因为(n - x, m - x)是可以由(n, m)转移所得到的, 很显然必败态只能转移到必胜态.
我们可以考虑枚举所有组合情况, 然后对于每个必败态, 我们筛出可以由该必败态转移到的必胜态.
这样复杂度为
O
(
n
3
l
o
g
n
)
O(n^3logn)
O(n3logn), 但是由于必败态的数量很少(至多5000个, 实际才1000多个), 因此实际复杂度为
O
(
n
2
l
o
g
n
)
O(n^2logn)
O(n2logn). (std代码跑得好快555, 可是我看不懂)
AC代码
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 5E3 + 10;
bitset<N> sg[N]; //所以为什么bitset又比bool数组快了呀? (用int基本必T)
void init(int n = 5000) {
for (int a = 0; a <= n; ++a) {
for (int b = 0; b <= n; ++b) {
if (sg[a][b]) continue; //必胜态
for (int k = 1; k + a <= n; ++k) { //枚举基数
for (int qaq = 0; qaq + b <= n; qaq += k) {
sg[a + k][b + qaq] = 1;
}
}
for (int k = 1; k + b <= n; ++k) { //枚举基数
for (int qaq = 0; qaq + a <= n; qaq += k) {
sg[a + qaq][b + k] = 1;
}
}
}
}
}
int main()
{
init();
int t; cin >> t;
while (t--) {
int a, b; scanf("%d %d", &a, &b);
puts(sg[a][b] ? "Alice" : "Bob");
}
return 0;
}
B. Ball Dropping
大致题意
给出一个倒的等腰梯形和一个圆.
问: 圆能否卡在梯形中, 如果可以则输出圆心到底边的距离, 反之输出"drop".
题目考查
数学 平面几何 难度: Easy
解题思路
我是菜鸡. 放一下队友代码 = 我会了.
AC代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve() {
ll r, a, b, h;
cin >> r >> a >> b >> h;
if (2 * r < b) puts("Drop");
else {
double x = 1.0 * b * h / (a - b);
double c = b / 2.0;
double y = sqrt(x * x + c * c);
double d = 2 * r * y / b - x;
printf("Stuck\n%.10f\n", d);
}
}
int main()
{
solve();
return 0;
}
C. Cut the Tree
D. Determine the Photo Position
大致题意
给出一个 n × n n \times n n×n的01矩阵, 其中0的位置表示空位.
问有多少种不同的方式摆放一个 1 × m 1 \times m 1×m的矩形.
题目考查
模拟 尺取 难度: Easy
解题思路
我们可以遍历每一行来获取当前连续0的长度. 如果当前长度>=m, 则结果+1即可.
AC代码
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 2E3 + 10;
char g[N][N];
int main()
{
int n, m; cin >> n >> m;
rep(i, n) scanf("%s", g[i] + 1);
string qaq; cin >> qaq;
int res = 0;
rep(i, n) {
int now = 0;
rep(j, n) {
if (g[i][j] == '0') {
now++;
if (now >= m) res++;
}
else now = 0;
}
}
cout << res << endl;
return 0;
}
E. Escape along Water Pipe
好漂亮的大模拟, 先不补了.
F. Find 3-friendly Integers
大致题意
定义: 一个整数的字符串形式中, 如果某个连续子串内部数字之和是3的倍数, 则称这个数字是友好数字.
问: [ l , r ] [l, r] [l,r]区间内, 有多少个友好数字.
题目考查
数论 找规律, 数位dp 难度: Easy
解题思路
本题是可以采用数位dp求解的. 但是下文以找规律方式解题.
结论: 位数大于等于3位的数字都是友好数字.
在模3意义下每一位的选取只有0, 1, 2三种. 我们假设数字x, 对于x我们分析每位可选数字情况.
对于第一位数字, 我们选择1或2都可以, 第二位数字我们必须同第一位的选择, 否则会出现 ( 1 + 2 ) % 3 = = 0 (1 + 2) \% 3 == 0 (1+2)%3==0的情况. 那么对于第三位的选择, 无论我们如何选取, 均会使得数字变为友好数字.
因此我们最终的做法对于1位数和2位数特判即可.
AC代码
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
bool fact(int x) { //不是友好数字
if (x < 10) return x % 3;
return x % 10 % 3 == x / 10 % 3 and x / 10 % 3;
}
int main()
{
int t; cin >> t;
while (t--) {
ll l, r; scanf("%lld %lld", &l, &r);
ll res = r - l + 1;
if (l <= 99) {
int cou = 0;
for (int i = l; i <= min(99ll, r); ++i) {
if (fact(i)) cou++;
}
res -= cou;
}
printf("%lld\n", res);
}
return 0;
}
G. Game of Swapping Numbers
大致题意
给定两个长度为n的序列 a a a和 b b b. 你必须交换 k k k次序列 a a a中两个不同位置的元素.
最终要求最大化 ∑ i = 1 n ∣ a i − b i ∣ \sum_{i = 1}^n|a_i - b_i| ∑i=1n∣ai−bi∣.
题目考查
贪心算法 难度: Hard
解题思路
考虑到最终结果计算涉及到绝对值. 我们考虑如何去掉绝对值符号.
我们可以这样来考虑: 最终对于答案的贡献可以看作是对于每一个
a
i
a_i
ai和
b
i
b_i
bi分配一个正负号. 且正负号的总数相同.
如果正负号不方便理解, 你可以广义的认为是对结果产生正向或负向的贡献.
如果我们抛去必须交换k次这个限制. 认为可以无限次进行交换, 则最终的结果应该是: 假设集合st = { 序列a中所有元素 ∪ 序列b中所有元素 }, 则答案为st中最大的n个元素 - st中最小的n个元素.
我们可以进行如下证明: 相当于我们只需要让最小的那n个元素取负号, 最大的n个元素取正号.
那如何让某个元素取负号呢? 例如我让 a i a_i ai取负号, 则我让 b i > a i b_i > a_i bi>ai即可, 这样我们发现 b i b_i bi也恰好可以取得正号. 这相当于, 我们让最小的n个元素的对侧为最大的n个元素即可. 通过不限次数的交换我们一定可以实现.
考虑到交换元素后对结果产生的影响:
我们以线段的形式来表示
a
i
a_i
ai和
b
i
b_i
bi对答案的贡献.
如果我们考虑交换位置
i
i
i和位置
j
j
j.
①首先考虑位置i和j值域有交集的情况. (如下图所示, 包括一个线段完全包含在另外一个线段的情况)
此时我们发现交换前和后, 总贡献都是 a j − b i + a i − b j a_j - b_i + a_i - b_j aj−bi+ai−bj, 总贡献没有改变.
此时我们发现交换前和后, 总贡献变少了.
我们发现: 第一张图的a与b, 相当于a数组的
a
i
a_i
ai与
a
j
a_j
aj处都是正号. 而第二张图则是一正一负.
进而推广我们可以得到结论: 在值域存在交集的情况下, 如果两个位置的
a
a
a同号, 则贡献不变, 反之贡献减少.
②考虑位置i和位置j值域无交集的情况.
(懒得画图了QAQ, 此处省略一张图)
此时我们发现无论位置如何选取, 总贡献一定会增加.
若假设
a
j
>
a
i
a_j > a_i
aj>ai 则增加的数值为:
m
i
n
(
a
j
,
b
j
)
−
m
a
x
(
a
i
,
b
i
)
min(a_j, b_j) - max(a_i, b_i)
min(aj,bj)−max(ai,bi).
综上所述, 我们可以进行如下分类:
①当n == 2时, 我们按照k的奇偶性直接得出答案即可.
②当n > 2时, 我们考虑进行至多 k k k次操作:
由于n > 2, 我们根据抽屉原理可以得出, 一定存在两个不同的
a
i
a_i
ai和
a
j
a_j
aj, 取相同符号, 则我们可以把题目中的 必须操作k次 转化为 之多操作k次.
对于k次操作, 我们可以每次贪心的交换对结果产生最大贡献的两个位置.
我们可以通过维护额外两个序列c和d. 其中 c i = m a x ( a i , b i ) c_i = max(a_i, b_i) ci=max(ai,bi), d i = m a x ( a i , b i ) d_i = max(a_i, b_i) di=max(ai,bi). 我们每次选取最大的 d i d_i di和最小的 c i c_i ci即可.
如果此时选取的 d i − c i < = 0 d_i - c_i <= 0 di−ci<=0, 则此时一定属于情况①.
AC代码
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 5E5 + 10;
int a[N], b[N], c[N], d[N];
int main()
{
int n, m; cin >> n >> m;
rep(i, n) scanf("%d", &a[i]);
rep(i, n) scanf("%d", &b[i]);
if (n == 2) {
if (m & 1) swap(a[1], a[2]);
int res = abs(a[1] - b[1]) + abs(a[2] - b[2]);
printf("%d\n", res);
}
else {
ll res = 0;
rep(i, n) {
res += abs(a[i] - b[i]);
c[i] = max(a[i], b[i]);
d[i] = min(a[i], b[i]);
}
sort(c + 1, c + 1 + n), sort(d + 1, d + 1 + n, greater<>());
rep(i, min(n, m)) {
int qaq = d[i] - c[i];
if (qaq <= 0) break;
res += 2 * qaq;
}
cout << res << endl;
}
return 0;
}
H. Hash Function
55555 不会FFT/NTT的弱鸡留下了卑微的泪水.
I. Increasing Subsequence
概率dp, dp弱鸡放弃思考. (疯狂暗示队友)
J. Journey among Railway Stations
大致题意
一段路上有
n
n
n个点, 每个点有一个合法时间段
[
L
i
,
R
i
]
[L_i, R_i]
[Li,Ri], 表示最早到达时间和最晚到达时间.
特别的: 其中如果到达某个点过早, 则可以等到L后再到达, 但是不能晚于R.
每两个点之间有一段距离 d i s dis dis, 如 d i s i dis_i disi表示第i个点到第i+1个点所需要花费的时间.
有m次操作:
① 0 l r
表示从点l出发, 依次经过所有点, 最终到达点r. 询问能否保证合法到达每一个点.
② 1 a c
修改
d
i
s
a
=
c
dis_a = c
disa=c.
③ 2 x l r
修改
L
i
=
l
,
R
i
=
r
L_i = l, R_i = r
Li=l,Ri=r.
题目考查
数据结构 线段树 难度: Hard
解题思路
对于这种题, 可以感性的尝试猜测是DS题. 我们首先考虑询问.
假设询问区间[2, 4]:
那么我们需要满足如下两个不等式:
①
m
a
x
(
l
2
+
d
i
s
2
,
l
3
)
<
=
r
3
,
②
m
a
x
(
m
a
x
(
l
2
+
d
i
s
2
,
l
3
)
+
d
i
s
3
,
l
4
)
<
=
r
4
\displaystyle①max(l_2 + dis_2, l_3) <= r_3, ②max(max(l_2 + dis_2, l_3) + dis_3, l_4) <= r_4
①max(l2+dis2,l3)<=r3,②max(max(l2+dis2,l3)+dis3,l4)<=r4, 特别的, 式中并非最简形式, 而是为了之后的化简.
我们对于上式进行变形, 把不等式②的 d i s 3 dis_3 dis3放入max: ② m a x ( m a x ( l 2 + d i s 2 + d i s 3 , l 3 + d i s 3 ) , l 4 ) < = r 4 \displaystyle②max(max(l_2 + dis_2 + dis_3, l_3 + dis_3), l_4) <= r_4 ②max(max(l2+dis2+dis3,l3+dis3),l4)<=r4
再进行变形: 对于不等式①两侧同时加上
∑
i
=
3
n
d
i
s
i
\displaystyle\sum_{i = 3}^n dis_i
i=3∑ndisi:
①
m
a
x
(
l
2
+
∑
i
=
2
n
d
i
s
i
,
l
3
+
∑
i
=
3
n
d
i
s
i
)
<
=
r
3
+
∑
i
=
3
n
d
i
s
i
\displaystyle①max(l_2 + \sum_{i = 2}^n dis_i, l_3 + \sum_{i = 3}^n dis_i) <= r_3 + \sum_{i = 3}^n dis_i
①max(l2+i=2∑ndisi,l3+i=3∑ndisi)<=r3+i=3∑ndisi
对于不等式①两侧同时加上
∑
i
=
4
n
d
i
s
i
\displaystyle\sum_{i = 4}^n dis_i
i=4∑ndisi:
②
m
a
x
(
m
a
x
(
l
2
+
∑
i
=
2
n
d
i
s
i
,
l
3
+
∑
i
=
3
n
d
i
s
i
)
,
l
4
+
∑
i
=
4
n
d
i
s
i
)
<
=
r
4
+
∑
i
=
4
n
d
i
s
i
\displaystyle②max(max(l_2 + \sum_{i = 2}^n dis_i, l_3 + \sum_{i = 3}^n dis_i), l_4 + \sum_{i = 4}^n dis_i) <= r_4 + \sum_{i = 4}^n dis_i
②max(max(l2+i=2∑ndisi,l3+i=3∑ndisi),l4+i=4∑ndisi)<=r4+i=4∑ndisi
最终我们得到不等式: { ① m a x ( l 2 + ∑ i = 2 n d i s i , l 3 + ∑ i = 3 n d i s i ) < = r 3 + ∑ i = 3 n d i s i ② m a x ( m a x ( l 2 + ∑ i = 2 n d i s i , l 3 + ∑ i = 3 n d i s i ) , l 4 + ∑ i = 4 n d i s i ) < = r 4 + ∑ i = 4 n d i s i \left\{\begin{aligned} ①max(l_2 + \sum_{i = 2}^n dis_i, l_3 + \sum_{i = 3}^n dis_i) <= r_3 + \sum_{i = 3}^n dis_i \\②max(max(l_2 + \sum_{i = 2}^n dis_i, l_3 + \sum_{i = 3}^n dis_i), l_4 + \sum_{i = 4}^n dis_i) <= r_4 + \sum_{i = 4}^n dis_i \end{aligned}\right. ⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧①max(l2+i=2∑ndisi,l3+i=3∑ndisi)<=r3+i=3∑ndisi②max(max(l2+i=2∑ndisi,l3+i=3∑ndisi),l4+i=4∑ndisi)<=r4+i=4∑ndisi
我们不妨设 l i ′ = l i + ∑ i = i n d i s i , r i ′ = r i + ∑ i = i n d i s i \displaystyle l'_i = l_i + \sum_{i = i}^n dis_i, r'_i = r_i + \sum_{i = i}^n dis_i li′=li+i=i∑ndisi,ri′=ri+i=i∑ndisi
则区间[2, 4]满足条件, 表明: ① m a x ( l 2 ′ , l 3 ′ ) < = r 3 ′ , ② m a x ( m a x ( l 2 ′ , l 3 ′ ) , l 4 ′ ) < = r 4 ′ ①max(l'_2, l'_3) <= r'_3, ②max(max(l'_2, l'_3), l'_4) <= r'_4 ①max(l2′,l3′)<=r3′,②max(max(l2′,l3′),l4′)<=r4′.
我们考虑把公式普适化: 若查询 [ l , r ] [l, r] [l,r]是否满足条件, 则对于区间的每个点而言, 到达下一节点的最早时间应 小于等于 下一节点允许到达的最晚时间. 而我们可以通过维护 m a x ( { l ′ } ) max(\{l'\}) max({l′})来得到当前点以为起始, 到达下一点的最早时间, 与下一点的最晚到达时间比较即可.
考虑区间可加性: 假设 [ l , m i d ] [l, mid] [l,mid]可达, 且 [ m i d + 1 , r ] [mid + 1, r] [mid+1,r]可达, 判断 [ l , r ] [l, r] [l,r]是否可达. 考虑到合并区间后, 本质是对右区间增加了一条限制: 左区间的 m a x ( { l ′ } ) max(\{l'\}) max({l′}) 应当小于 右区间的 m i n ( { r ′ } ) min(\{ r'\}) min({r′}).
因此我们发现我们可以用线段树维护区间查询操作.
考虑到两种修改操作, 其实就是线段树的单点修改和区间修改. 感觉不是重点就不多说了.
AC代码
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 1E6 + 10;
int L[N], R[N];
int dis[N]; ll sum[N];
struct node {
int l, r;
ll early, late; bool flag; //early维护max({l'}), late维护min({r'}).
ll lazy;
}t[N << 2];
void pushdown(node& op, ll lazy) {
op.early += lazy, op.late += lazy;
op.lazy += lazy;
}
void pushdown(int x) {
if (!t[x].lazy) return;
pushdown(t[x << 1], t[x].lazy), pushdown(t[x << 1 | 1], t[x].lazy);
t[x].lazy = 0;
}
void pushup(node& p, node& l, node& r) {
p.early = max(l.early, r.early);
p.late = min(l.late, r.late);
p.flag = l.flag and r.flag and l.early <= r.late;
}
void pushup(int x) { pushup(t[x], t[x << 1], t[x << 1 | 1]); }
void build(int l, int r, int x = 1) {
t[x] = { l, r, L[l] + sum[l], R[l] + sum[l], 1, 0 };
if (l == r) return;
int mid = l + r >> 1;
build(l, mid, x << 1), build(mid + 1, r, x << 1 | 1);
pushup(x);
}
void modifydis(int l, int r, int c, int x = 1) {
if (l <= t[x].l and r >= t[x].r) {
pushdown(t[x], c);
return;
}
pushdown(x);
int mid = t[x].l + t[x].r >> 1;
if (l <= mid) modifydis(l, r, c, x << 1);
if (r > mid) modifydis(l, r, c, x << 1 | 1);
pushup(x);
}
void modifyarea(int a, int cl, int cr, int x = 1) {
if (t[x].l == t[x].r) {
t[x].early += cl, t[x].late += cr;
return;
}
pushdown(x);
int mid = t[x].l + t[x].r >> 1;
modifyarea(a, cl, cr, x << 1 | (a > mid));
pushup(x);
}
auto ask(int l, int r, int x = 1) {
if (l <= t[x].l and r >= t[x].r) return t[x];
pushdown(x);
int mid = t[x].l + t[x].r >> 1;
if (r <= mid) return ask(l, r, x << 1);
if (l > mid) return ask(l, r, x << 1 | 1);
node res = { 0, 0, 0, 0, 0, 0 };
node left = ask(l, r, x << 1), right = ask(l, r, x << 1 | 1);
pushup(res, left, right);
return res;
}
int main()
{
int T; cin >> T;
while (T--) {
int n; scanf("%d", &n);
rep(i, n) scanf("%d", &L[i]);
rep(i, n) scanf("%d", &R[i]);
rep(i, n - 1) scanf("%d", &dis[i]);
sum[n] = 0;
for (int i = n - 1; i >= 1; --i) sum[i] = sum[i + 1] + dis[i];
build(1, n);
int m; scanf("%d", &m);
rep(i, m) {
int tp; scanf("%d", &tp);
if (tp == 0) {
int l, r; scanf("%d %d", &l, &r);
auto qaq = ask(l, r).flag;
puts(qaq ? "Yes" : "No");
}
else if (tp == 1) {
int a, c; scanf("%d %d", &a, &c);
modifydis(1, a, c - dis[a]);
dis[a] = c;
}
else {
int x, l, r; scanf("%d %d %d", &x, &l, &r);
modifyarea(x, l - L[x], r - R[x]);
L[x] = l, R[x] = r;
}
}
}
return 0;
}
K. Knowledge Test about Match
大致题意
随机生成一个长度为
n
n
n的序列b, 各元素取值为
[
0
,
n
−
1
]
[0, n - 1]
[0,n−1].
现在你有一个序列$a = { 0,1,2,…,n−2,n−1 } $.
你可以任意重排列序列b, 要求最小化 ∑ i = 0 n − 1 ∣ a i − b i ∣ \displaystyle\sum_{i = 0}^{n - 1} \sqrt{|a_i - b_i|} i=0∑n−1∣ai−bi∣. (注: 题中序列编号从0~n-1)
注: 题目中允许与最优的匹配方式有4%的误差.
题目考查
最大完美匹配 数论 贪心 难度: Hard
解题思路
本题看大佬们可以用KM算法解题. 但本思路提供一种贪心的方式.
考虑到我们需要最小化的结果是一个平方根累加和. 我们注意到平方根的函数性质是: 一起初增量很大, 最终趋于平稳.
因此我们的贪心核心思路是: 尽可能避免较小的差值情况.
考虑匹配部分序列: { 4, 6 } 和 { 1, 3 }
我们一种匹配方式是: (4, 1), (6, 3), 结果为: 2 3 ≈ 3.46 2\sqrt3 \approx 3.46 23≈3.46 .
另外一种匹配方式是: (4, 3), (6, 1), 结果为: 1 + 5 ≈ 3.23 1 + \sqrt5 \approx 3.23 1+5≈3.23.很显然方法二更优. 因此我们并不可以直接按照小值取匹配小值的方式进行匹配.
做法: 我们开个桶来记录每个数字的出现次数, 从小到大枚举差值, 记录当前差值情况满足要求的序列位置.
由于保证b序列是随机的, 因此我们可以认为该做法不被特殊数据卡掉.
AC代码
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 1E3 + 10;
int cou[N], a[N];
int main()
{
int t; cin >> t;
while (t--) {
int n; scanf("%d", &n);
rep(i, n) {
int x; scanf("%d", &x);
cou[x + 1]++;
a[i] = 0;
}
for (int cha = 0; cha <= n; ++cha) {
rep(i, n) {
if (a[i]) continue;
if (i - cha >= 1 and cou[i - cha]) {
cou[i - cha]--, a[i] = i - cha;
}
else if (i + cha <= n and cou[i + cha]) {
cou[i + cha]--, a[i] = i + cha;
}
}
}
rep(i, n) printf("%d%c", a[i] - 1, " \n"[i == n]);
}
return 0;
}