Codeforces Round #681
写在前面
Codeforces Round #681 (Div. 2, based on VK Cup 2019-2020 - Final)
比赛地址:https://codeforces.com/contest/1443。
选择去厕所门口嗨而不是把 SB E 写完的 Lb 真是人间之鉴。
以及这场直接是暴力大赛= =
A
Link。
\(t\) 组询问,每次给定整数 \(n\),构造一个长度为 \(n\) 的数列 \(a\),满足:
- \(\forall 1\le i\le n,\ 1\le a_i\le 4n\)。
- \(\forall 1\le i,j\le n,\ \gcd(i,j)\not= 1,\ a_i\nmid a_j,\ a_j\nmid a_i\)。
\(1\le t,n\le 100\)。
2S,256MB,SPJ。
贪心。
一种容易想到的合法构造方案是:
//知识点:贪心
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
//=============================================================
//=============================================================
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;
}
//=============================================================
int main() {
int t = read();
while (t --) {
int n = read();
for (int i = 4 * n; n; i -= 2) {
printf("%d ", i);
-- n;
}
printf("\n");
}
return 0;
}
B
Link。
\(t\) 组数据,每次给定一长度为 \(n\) 的 \(01\) 序列,给定两种操作:
- 选择一段连续的 \(1\),花费 \(a\) 的代价将它们全变为 \(0\)。
- 花费 \(b\) 的代价,使一个 \(0\) 变为 \(1\)。
求使整个序列都变成 \(0\) 的最小代价。
\(1\le t\le 10^5\),\(1\le \sum n\le 10^5\),\(1\le a,b\le 10^3\)。
2S,256MB。
贪心。
如果有两段相邻的连续的 \(1\),削除它们的方法有两种。
一是进行 2 次操作 1,花费为 \(2a\)。
二是先进行多操作 2 使它们相连,再进行操作 1,花费为 \(a+kb\)。
于是从左向右贪心,若两段 1 的距离乘 \(b\) 小于 \(a\) 则用第二种方法,否则用第一种。
注意实现。
//知识点:贪心
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
char s[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;
}
//=============================================================
int main() {
int t = read();
while (t --) {
int a = read(), b = read();
int n, ans = 0, cnt0 = a; //赋初值,处理序列开头是一堆 0 的情况。
scanf("%s", s + 1);
n = strlen(s + 1);
for (int i = 1; i <= n; ++ i) {
if (s[i] == '1' && s[i - 1] != '1') {
ans += std::min(cnt0 * b, a);
cnt0 = 0;
continue ;
}
cnt0 += (s[i] == '0');
}
printf("%d\n", ans);
}
return 0;
}
/*
1
1 1
11111
*/
C
Link。
\(t\) 组数据,每次给定 \(n\) 个物品。
对于第 \(i\) 个物品,可以选择快递运输,花费 \(a_i\) 时间。或上门自取,花费 \(b_i\) 时间。
快递是同时出发的,上门自取时间是累加的。
求最小的总时间花费。
\(1\le t\le 2\times 10^5\),\(1\le \sum n\le 2\times 10^5\),\(1\le a_i,b_i\le 10^9\)。
2S,256MB。
二分答案。
一开始我尝试把题意抽象成一堆具有 2 个属性的物品,但发现丢失了时间这个重要条件后,做法就不显然了,于是改了回去= =
显然二分答案,枚举总时间花费 \(mid\)。
对于一个物品 \((a_i,b_i)\),若快递运输的时间 \(\le mid\),则使用快递运输,否则上门自取。
检查上门自取时间是否不大于 \(mid\) 即可。
//知识点:二分答案
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 2e5 + 10;
const LL kInf = 1e15 + 2077;
//=============================================================
int n, a[kN], b[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;
}
bool Check(LL lim_) {
LL sum = 0;
for (int i = 1; i <= n; ++ i) {
if (1ll * a[i] <= lim_) continue ;
sum += 1ll * b[i];
}
return sum <= lim_;
}
//=============================================================
int main() {
int t = read();
while (t -- ){
n = read();
for (int i = 1; i <= n; ++ i) a[i] = read();
for (int i = 1; i <= n; ++ i) b[i] = read();
LL ans = 0;
for (LL l = 1, r = kInf; l <= r; ) {
LL mid = (l + r) >> 1;
if (Check(mid)) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
printf("%lld\n", ans);
}
return 0;
}
D
Link。
\(t\) 组数据,每次给定一长度为 \(n\) 的数列 \(a\),有两种操作:
- 选择任意整数 \(k\ (1\le k\le n)\),使 \(a_1 \sim a_k\) 减去 \(1\)。
- 选择任意整数 \(k\ (1\le k\le n)\),使 \(a_k \sim a_n\) 减去 \(1\)。
求经过任意次操作后,能否使得所有数全变为 \(0\)。
\(1\le t,\sum n\le 3\times 10^4\),\(1\le a_i\le 10^6\)。
2S,256MB。
差分,贪心。
区间操作先考虑差分,设 \(a\) 的差分数组为 \(b\),有 \(b_i = a_i - a_{i-1}\)。
问题变为能否令 \(b_1\sim b_n\) 全变为 \(0\)。
考虑两种操作对差分数组的影响。
操作 1 令 \(b_1 - 1\),\(b_{k+1} +1\),操作 2 令 \(b_k - 1\),\(b_{n+1} + 1\)。
对 \(b_{n+1}\) 的操作不会影响答案,则可以通过若干次操作 2 使 \(b_2 \sim b_n\) 中所有大于 0 的元素变为 0。
再考虑操作 1,显然应通过操作 1 将所有不大于 0 的 \(b_i\) 调整为 0。若 \(b_1\sim b_n\) 中所有小于 0 的元素的绝对值之和是否大于 \(a_1\),显然无解。若小于 0,则可以通过操作 2 将 \(a_1\) 再调整为 0。
于是仅需检查 \(b_1\sim b_n\) 中所有小于 0 的元素的绝对值之和是否不大于 \(a_1\) 即可。
//知识点:差分,贪心
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 3e4 + 10;
//=============================================================
int n, a[kN], b[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;
}
//=============================================================
int main() {
int t = read();
while (t --) {
n = read();
for (int i = 1; i <= n; ++ i) {
a[i] = read();
b[i] = a[i] - a[i - 1];
if (b[i] < 0) a[1] += b[i];
}
printf("%s\n", a[1] >= 0 ? "YES" : "NO");
}
return 0;
}
E
Link。
对于一 \(1\sim n\) 的排列,定义其排名为在 \(1\sim n\) 的全排列中,它的字典序的排名。
有一 \(1\sim n\) 的排列 \(a\),初始时有 \(\forall 1\le i\le n,\ a_i = i\)。有 \(m\) 次操作:
- 查询 \([l,r]\) 的区间和。
- 令排列 \(a\) 的排名增加 \(x\)。
\(2\le n,m\le 2\times 10^5\),\(1\le x\le 10^5\),保证 \(\sum x< n!\)。
4S,256MB。
暴力。
发现操作 2 不好维护,但观察数据范围,发现有 \(\sum x\le 2\times 10^{10}\),且有 \(14!>2\times 10^{10}\)。
则经过若干次操作 2 后,只有最后 \(14\) 个位置会有变化。
则对前 \(n-14\) 个位置维护一个前缀和,后 \(14\) 个位置独立出来。
查询操作根据 \([l,r]\) 是否包含 \(n-14\) 分类讨论,前面部分用前缀和查询,后面直接暴力。
修改操作暴力改后 \(14\) 个位置即可。
关于怎么暴力维护:
如果了解康托展开的话可以直接逆展开一下。
如果嫌麻烦可打表找下规律:
1234
1243
----
1324
1342
----
1423
1432
--------------------------
2134
2143
----
2314
2341
----
2413
2431
--------------------------
3124
3142
----
3214
3241
----
3412
3421
--------------------------
4123
4132
----
4213
4231
----
4312
4321
有 \(3!=6\),\(2!=2\),规律非常显然,预处理阶乘,从字典序中从大到小减去阶乘,即可得到每一位的元素。
注意实现时的细节。
//知识点:暴力
/*
By:Luckyblock
爆炸阶乘!
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, m, a[kN], tmp[20];
LL sumx = 1, fac[20], sum[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 Prepare() {
n = read(), m = read();
fac[0] = 1;
for (int i = 1; i <= n; ++ i) {
if (i <= 14) fac[i] = 1ll * fac[i - 1] * i;
sum[i] = 1ll * sum[i - 1] + i;
a[i] = i;
}
}
LL Query(int l_, int r_) {
LL ret = 0;
if (r_ - l_ <= 14) { //可以直接暴力
for (int i = l_; i <= r_; ++ i) ret += 1ll * a[i];
return ret;
}
if (r_ < n - 14) { //特判
ret = sum[r_] - sum[l_ - 1];
} else { //暴力
ret += sum[n - 15] - sum[l_ - 1];
for (int i = n - 14; i <= r_; ++ i) ret += 1ll * a[i];
}
return ret;
}
void Modify() {
int lim = std::min(14, n); //注意枚举上界
LL x = sumx;
bool vis[20] = {0};
for (int i = lim - 1; i >= 1; -- i) {
for (int j = 1; j <= lim; ++ j) { //不断减去 i!,确定元素。
if (vis[j]) continue;
if (x > fac[i]) {
x -= fac[i];
continue;
}
tmp[lim - i] = j; //填入
vis[j] = true;
break;
}
}
for (int j = 1; j <= lim; ++ j) { //最后一个元素
if (! vis[j]) {
tmp[lim] = j;
break;
}
}
if (n <= 14) { //暴力改
for (int i = 1; i <= n; ++ i) a[i] = tmp[i];
} else {
for (int i = 1; i <= 14; ++ i) {
a[n - 14 + i] = tmp[i] + n - 14;
}
}
// printf("\n\n\n");
// for (int i = 1; i <= n; ++ i) printf("%d ", a[i]);
// printf("\n\n\n");
}
//=============================================================
int main() {
Prepare();
while (m --) {
int opt = read();
if (opt == 1) {
int l = read(), r = read();
printf("%lld\n", Query(l, r));
} else {
sumx += 1ll * read(); //更新字典序
Modify();
}
}
return 0;
}
/*
16 3
1 1 16
2 1000000
1 15 16
*/
F
Link。
\(t\) 组数据,每次给定一 \(1\sim n\) 的排列 \(a\),一长度为 \(m\) 的数列 \(b\),满足 \(b\) 中元素不重复,且 \(\forall 1\le i\le m,\ \exists 1\le j\le n,\ b_i = a_j\)。
需要进行 \(m\) 次操作,每次操作从数列 \(a\) 中选择一个下标 \(t\),选择 \(a_{t-1}\) 或 \(a_{t+1}\) 加入 \(c\) 数列的末尾,然后删除 \(a_t\)。
求有多少种操作方案,能使数列 \(c\) 等于 数列 \(b\),答案对 \(998244353\) 取模。
\(1\le t\le 10^5\),\(1\le m\le n\le 2\times 10^5\),\(1\le \sum n\le 2\times 10^5\)。
2S,512MB。
暴力。
\(a,b\) 中的数都是不重的,则对于 \(b\) 中所有数,用 \(b_i\) 在 \(a\) 中的位置替换它。
把这个操作转化一下,等价于每次在 \(a\) 中选择一个 \(b\) 中的数,选择左右两侧的一个删除,在将该数加入到 \(c\) 的末尾。
考虑顺序枚举 \(b\) 加数,设当前加到 \(b_i\),讨论 \(a_{b_i-1}\) 和 \(a_{b_i+1}\) 的情况。
- 若 \(a_{b_i -1}\) 和 \(a_{b_i + 1}\) 均在 \(b\) 中,则不能删去任何一个,无解。
- 若 \(a_{b_i - 1}\) 和 \(a_{b_i + 1}\) 中仅有一个不在 \(b\) 中,显然应删除另一个,删除的方案数为 1。
- 若 \(a_{b_i -1}\) 与 \(a_{b_i + 1}\) 均不在 \(b\) 中,则可以删除任意一个。因为删除后 \(a_{b_i}\) 也可以删除,对原数列的影响相同。则删除的方案数为 2。
根据上述情况,用链表模拟删除即可。总复杂度 \(O\left(\sum n\right)\)。
注意特判一些边界情况。
不能用 memset
,否则 TLE 警告。
//知识点:暴力
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 2e5 + 10;
const int mod = 998244353;
//=============================================================
struct Data {
int val, l, r;
} a[kN];
int n, m, ans, b[kN], posa[kN];
bool tag[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 Delete(int pos_) {
if (a[pos_].l) a[a[pos_].l].r = a[pos_].r;
if (a[pos_].r) a[a[pos_].r].l = a[pos_].l;
}
void Init() {
n = read(), m = read();
ans = 1;
for (int i = 0; i <= n + 1; ++ i) {
a[i] = (Data) {0, 0, 0};
posa[i] = tag[i] = 0;
}
tag[0] = tag[n + 1] = true;
for (int i = 1; i <= n; ++ i) {
a[i] = (Data) {read(), i - 1, i + 1};
posa[a[i].val] = i;
}
for (int i = 1; i <= m; ++ i) {
b[i] = read();
tag[b[i]] = i;
}
}
//=============================================================
int main() {
int t = read();
while (t --) {
Init();
for (int i = 1; i <= m; ++ i) {
int val = b[i], pos = posa[val];
if (tag[a[a[pos].l].val] && tag[a[a[pos].r].val]) {
ans = 0;
break ;
} else if (tag[a[a[pos].l].val]) {
Delete(a[pos].r);
} else if (tag[a[a[pos].r].val]) {
Delete(a[pos].l);
} else {
ans = 2ll * ans % mod;
Delete(a[pos].r ? a[pos].r : a[pos].l);
}
tag[val] = false;
}
printf("%d\n", ans);
}
return 0;
}
总结
- 区间操作先考虑差分。
- 注意观察数据范围,结合数据范围和题目性质,能够得到一些结论。
- 多测直接
memset
清空数组的全部必定 TLE,应用 for 清空,用到多少清空多少。 - 注意链表的边界情况,注意特判。