CF Round 776 Div3 题解
A题 Deletions of Two Adjacent Letters (签到,小思维)
给定一个字符串 \(s\) 和一个字符 \(c\)。
我们可以进行若干次操作,每次删除字符串中两个相邻字符(例如把 sasyy 删成 syy 或者 say 或者 sas)啥的。问我们能否将字符串删成 \(c\) ?
我们记字符串下标为 \([1,n]\),显然,如果存在 \(1\leq i \leq n\) 且 \(s_i=c\),且 \(i-1,n-i\) 均为偶数,那么则能。
#include<bits/stdc++.h>
using namespace std;
string s;
char c;
bool solve() {
cin >> s >> c;
for (int i = 0; i < s.length(); ++i)
if (s[i] == c) {
int len1 = i, len2 = s.length() - i - 1;
if (len1 % 2 == 0 && len2 % 2 == 0)
return true;
}
return false;
}
int main()
{
int T;
cin >> T;
while (T--) puts(solve() ? "YES" : "NO");
return 0;
}
B题 DIV + MOD (数学)
记 \(\operatorname{f}_a(x)=\lfloor\frac{x}{a}\rfloor+x\bmod a\),求 \(\max\limits_{l\leq i\leq r}\operatorname{f}_a(i)\)。
\(1\leq l\leq r \leq 10^9,1\leq a\leq 10^9\)
我们记一个数 \(x=ka+b\),那么我们显然要让 \(k+b\) 最大化。
显然,我们应该让 \(b\) 尽可能的大:因为增加 \(b\) 只需要花费 1 的代价,但是增加 \(k\) 则需要增加 \(a\) 的代价,怎么看都是先让 \(b\) 增加更划算。也就是说,在可能的情况下,我们应该让 \(b=a-1\),然后记 \(k=\lfloor\frac{r-b}{a}\rfloor\) ?
我们还要考虑一下 \(l\) 的范围,倘若上面的那个值已经小于 \(l\),说明 \(\lfloor\frac{l}{a}\rfloor=\lfloor\frac{r}{a}\rfloor\),即 \(k_1=k_2\),那直接输出 \(k_2+b_2\) 就完事了。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
LL solve() {
LL L, R, a;
cin >> L >> R >> a;
if (L / a == R / a) return R / a + R % a;
return (a - 1) + (R - a + 1) / a;
}
int main()
{
int T;
cin >> T;
while (T--) cout << solve() << endl;
return 0;
}
C题 Weight of the System of Nested Segments (排序)
给定 \(m\) 个点,第 \(i\) 个点的位置为 \(x_i\),权值为 \(w_i\)。没有点的位置是重合的。
现在我们尝试选取 \(n\) 个点对,第 \(i\) 个点对为 \((a_i,b_i)\) (\(a_i,b_i\) 是那个点的对应位置)。要求对于点对 \(i\) 和点对 \(j\),若 \(i < j\),那么必须有 \(a_i<a_j<b_j<a_i\)。(如果这个看不是很懂,可以参考一下原题面的那个图)
在点对位置满足情况的同时,我们需要让点对的权值之和最小化,并输出方案。
\(n\leq 10^5,2n\leq m\leq 2*10^5,-10^9\leq x_i\leq 10^9,-10^4\leq w_i\leq 10^4\),方案任意输出其中一种即可(先是最小化的权值,然后依次是第 \(i\) 个点对的信息,输出的是那个点对在原给定数组中的编号)
我们显然通过排序找到权值最小的 \(2n\) 个点,然后对他们排个序,配对就行了。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 200010;
int n, m;
struct Node { LL x, w, id; };
bool cmp1(Node a, Node b) { return a.w < b.w; }
bool cmp2(Node a, Node b) { return a.x < b.x; }
Node arr[N];
void solve() {
//read
cin >> n >> m;
for (int i = 1; i <= m; ++i) {
cin >> arr[i].x >> arr[i].w;
arr[i].id = i;
}
//solve
sort(arr + 1, arr + m + 1, cmp1);
sort(arr + 1, arr + 2 * n + 1, cmp2);
LL ans = 0;
for (int i = 1; i <= 2 * n; ++i)
ans += arr[i].w;
//output
cout << ans << endl;
for (int i = 1; i <= n; ++i)
cout << arr[i].id << " " << arr[2 * n - i + 1].id << endl;
}
int main()
{
int T;
cin >> T;
while (T--) solve();
return 0;
}
D题 Twist the Permutation (构造)
给定一个排列 \(\{a_n\}\),初始状态下 \(a_i=i\)。
我们现在可以按顺序进行 \(n\) 轮操作,在第 \(i\) 轮操作中,我们可以进行任意次移位(每次移位都会将第 \(i\) 个值移到整个数列最左边,然后前 \(i-1\) 个值向右移)。
现在给定经过 \(n\) 轮操作后的排列 \(\{a_n\}\),那么我们需要构造出一个方案,使得从原初始数列能变为这个给定的数列?
\(n\leq 2*10^3\)
我们观察题目给的模拟,会发现:只有在第 \(i\) 轮的时候,才会将位置 \(i\) 上的 \(i\) 值给移动到其他位置。同样的,我们从给定的数列去还原,显然也是逆序处理,在第 \(i\) 轮将 \(i\) 移动回位置 \(i\)。
因为 \(n\) 范围不大,所以直接 \(O(n^2)\) 处理即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 2010;
int a[N], b[N], ans[N];
void change(int n, int p) {
int cnt = 0;
for (int i = p + 1; i <= n; ++i)
b[++cnt] = a[i];
for (int i = 1; i < p; ++i)
b[++cnt] = a[i];
b[++cnt] = a[p];
for (int i = 1; i <= n; ++i) a[i] = b[i];
}
void solve() {
int n;
//read
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
//solve
for (int i = n; i >= 1; --i)
for (int j = 1; j <= i; ++j)
if (a[j] == i) {
change(i, j);
ans[i] = (j == i) ? 0 : j;
break;
}
//output
for (int i = 1; i <= n; ++i)
cout << ans[i] << " ";
cout << endl;
}
int main()
{
int T;
cin >> T;
while (T--) solve();
return 0;
}
E题 Rescheduling the Exam (二分)
现在有一个长度为 \(d\) 的日程表(下标 \([1,d]\))。我们有 \(n\) 场考试,第 \(i\) 场考试的时间为 \(a_i\)(考试时间互不相同)。
考试之间我们需要休息,我们记两场考试之间的相隔天数的最小值为 \(x\)(还有一个第 0 场考试,记为 \(a_0=0\))。为了多休息,我们可以尝试改变某一次考试的时间,将其插到其他地方(也可以不改)。问修改后(或者不改)\(x\) 的最大值可能是多少。
\(2\leq n\leq 2*10^5,1\leq d\leq 10^9,1\leq a_i<a_{i+1}\leq d\)
这个 \(x\) 显然具有单调性质,所以我们去二分它。
我们来看看怎么 \(O(n)\) 的 check:我们依次枚举 \(i\),当 \(a_i-a_{i-1}-1<x\) 的时候,说明我们需要尝试进行调整,那么有:
-
移动 \(i\)
我们先看看能不能将 \(a_i\) 在 \(a_{i+1},a_{i-1}\) 间来回移动(\(a_{i+1}-a_{i-1}-1\geq 2x+1\)),然后再看看其他线段内能不能插进去,最后看看能不能给放到 \(d\) 的位置上面
-
移动 \(i-1\)
我们先看看能不能将 \(a_{i-1}\) 在 \(a_{i-2},a_{i}\) 间来回移动(\(a_{i}-a_{i-2}-1\geq 2x+1\)),然后再看看其他线段内能不能插进去,最后看看能不能给放到 \(d\) 的位置上面
这两个移动都有特例需要处理(移动 \(i\) 需要考虑 \(i=n\) 的情况,移动 \(i-1\) 需要考虑 \(i-1=0\) 的情况),注意针对搞一手。
说起来这题思路就这样,但是昨晚这题起码调了我一个小时就离谱(现在还复现不出来了),很烦。
#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, d, a[N];
bool check1(int x) {
bool flag = false;
int MAXL = 0, Last = 0;
for (int i = 1; i < n; ++i) {
if (a[i] - Last - 1 < x) {
//used
if (flag) return false;
//try to move
if (MAXL >= 2 * x + 1) flag = true;
if (a[i + 1] - Last - 1 >= 2 * x + 1)
flag = true;
for (int j = i + 2; j <= n; ++j)
if (a[j] - a[j - 1] - 1 >= 2 * x + 1)
flag = true;
if (d - a[n] - 1 >= x) flag = true;
if (!flag) return false;
}
else Last = a[i], MAXL = max(MAXL, a[i] - a[i - 1] - 1);
}
if (a[n] - Last - 1 < x) {
if (flag) return false;
return d - a[n - 1] - 1 >= x || MAXL >= 2 * x + 1;
}
return true;
}
bool check2(int x) {
bool flag = false;
int MAXL = 0;
if (a[1] - 1 < x) return false;
for (int i = 2; i <= n; ++i)
if (a[i] - a[i - 1] - 1 < x) {
//used
if (flag) return false;
//try to move
if (MAXL >= 2 * x + 1) flag = true;
if (a[i] - a[i - 2] - 1 >= 2 * x + 1)
flag = true;
for (int j = i + 1; j <= n; ++j)
if (a[j] - a[j - 1] - 1 >= 2 * x + 1)
flag = true;
if (d - a[n] - 1 >= x) flag = true;
if (!flag) return false;
}
else MAXL = max(MAXL, a[i] - a[i - 1] - 1);
return true;
}
int solve() {
cin >> n >> d;
for (int i = 1; i <= n; ++i)
cin >> a[i];
int l = -1, r = d;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (check1(mid) || check2(mid)) l = mid;
else r = mid - 1;
}
return l;
}
int main()
{
int T;
cin >> T;
while (T--) cout << solve() << endl;
return 0;
}
F题 Vitaly and Advanced Useless Algorithms (贪心,背包)
现在有 \(n\) 个待完成的任务,第 \(i\) 个任务还剩下 \(a_i\) 的时间去完成,每个任务的初始进度为 0。
现在我们有 \(m\) 个”秘籍“,每个秘籍可以表示为 \((e_i,t_i,p_i)\),表示花费 \(t_i\) 时间,给任务 \(e_i\) 的进度增加 \(p_i\)。当一个任务的进度达到 100 及以上的时候,这个任务被视为完成。
如果存在一个方案可以在截止前完成所有任务,输出该方案,反之则输出 -1。
有一个贪心:比起几个任务来回换着做,不如彻底做完一个任务之后再去做下一个。(我们可以从两个任务扩展到若干个)
针对每个任务,我们只考虑何其相关的秘籍,那么就变成了另一个问题:在尽可能少的时间内让进度达到 100 或以上。这是一个类似 01背包的 DP问题,可以分别求解。
现在,我们分别得到了每个任务的截止时间和需要花费的时间,接下来就是合理排列,使得每个任务都得完成。显然,我们直接按照截止时间来从小到大排序即可。
这题说起来容易,但是写起来属实是比较逆天的,例如那个背包对于 100 特殊处理(它可能从 \([100 - p_i,100]\) 中任意一点转移过来),还有必须得开一个 pre 数组从而记录方案,加上多组数据啥的,写的麻麻的。
#include<bits/stdc++.h>
using namespace std;
const int N = 100010, INF = 0x3f3f3f3f;
int n, m, a[N];
struct Node { int t, p, id; };
vector<Node> opt[N];
void read() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
opt[i].clear();
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
for (int i = 1; i <= m; ++i) {
int e, t, p;
scanf("%d%d%d", &e, &t, &p);
opt[e].push_back((Node){t, p, i});
}
}
//
struct Node2 { int val, u; } pre[N][110];
int t[N], p[N], id[N], dp[N][110];
vector<int> getMinTime(int Mission, int &Flag, int &vals) {
//init
int n = 0;
for (Node op : opt[Mission])
t[++n] = op.t, p[n] = op.p, id[n] = op.id;
//dp
for (int i = 0; i <= n; ++i)
for (int j = 0; j <= 100; ++j)
dp[i][j] = INF;
dp[0][0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 0; j < 100; ++j) {
dp[i][j] = dp[i - 1][j], pre[i][j] = (Node2){j, 0};
if (j >= p[i])
if (dp[i][j] > dp[i - 1][j - p[i]] + t[i])
dp[i][j] = dp[i - 1][j - p[i]] + t[i], pre[i][j] = (Node2){j - p[i], 1};
}
dp[i][100] = dp[i - 1][100], pre[i][100] = (Node2){100, 0};
for (int j = 100 - p[i]; j <= 100; ++j)
if (dp[i][100] > dp[i - 1][j] + t[i])
dp[i][100] = dp[i - 1][j] + t[i], pre[i][100] = (Node2){j, 1};
}
//
if (dp[n][100] == INF) {
Flag = false;
return {};
}
int val = 100;
vector<int> vec;
for (int k = n; k >= 1; k--) {
if (pre[k][val].u) vec.push_back(id[k]);
val = pre[k][val].val;
}
Flag = true, vals = dp[n][100];
return vec;
}
int flag, cnt, ans[N];
void solve() {
flag = true, cnt = 0;
int used = 0;
for (int i = 1; i <= n; ++i) {
int Flag, vals;
vector<int> cs = getMinTime(i, Flag, vals);
if (!Flag || used + vals > a[i]) {
flag = false;
return;
}
for (int x : cs) ans[++cnt] = x;
used += vals;
}
}
void output() {
if (!flag) {
puts("-1");
return;
}
printf("%d\n", cnt);
for (int i = 1; i <= cnt; ++i)
printf("%d ", ans[i]);
puts("");
}
int main()
{
int T;
scanf("%d", &T);
while (T--) {
read();
solve();
output();
}
return 0;
}
G题 Counting Shortcuts (BFS,最短路,图上计数)
给定一个 \(n\) 点 \(m\) 边的无向无边权联通路,记 \(s\) 到 \(t\) 的最短路长度为 \(d\),问 \(s,t\) 之间长度为 \(d\) 和 \(d+1\) 的路径有多少?
\(n.m\leq 2*10^5\),无自环,重边
换言之,我们要在这个无边权的无向图上求出起点到终点的最短路和次短路的数量。
最短路的计数还好,洛谷上有个题:P1144 最短路计数,解法是 BFS 一遍,然后类似 DAG 上的 DP 计数。
接下来,我们考虑次短路:因为次短路只比最短路增加了 1,而最短路是一个简单路径,所以次短路也应该是一个简单路径(没有环)。没有环意味着,这也满足 DAG 的性质,那我们理论上也可以参照 DP 的形式来计数。
我们开一个优先队列的节点:
struct Node {
//我们记最短路为0,次短路为1
int x, dis, type;
};
我们记 \(dist_{x,0/1},cnt_{x,0/1}\) 表示从起点到终点的最短路和次短路长度和数量,那么我们可以进行如下转移:
对于点 \(u\),记当前其计数为 \(count\),距离起点距离为 \(diatance\),那么我们枚举 \(v\in G[u]\):
- \(dist[v][0]>distance+w\),那么我们可以更新最短路,且原来的最短路变成了现在的次短路
- \(dist[v][0]=distance+w\),更新最短路计数即可
- \(dist[v][1]>distance+w\),更新次短路
- \(dist[v][1]=distance+w\),更新次短路计数即可
在此基础上,我们跑一次 Dijkstra 即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 200010, mod = 1e9 + 7;
struct Node {
int x, dis, type;
bool operator < (const Node &rhs) const {
return dis > rhs.dis;
}
};
priority_queue<Node> q;
vector<int> G[N];
int n, m, s, t;
int dis[N][2], cnt[N][2], vis[N][2];
int solve() {
//read
cin >> n >> m >> s >> t;
for (int i = 1; i <= n; ++i)
G[i].clear();
for (int i = 1; i <= m; ++i) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
//dijkstra
for (int i = 1; i <= n; ++i)
for (int j = 0; j < 2; ++j)
dis[i][j] = 0x3f3f3f3f, cnt[i][j] = vis[i][j] = 0;
q.push((Node){s, 0, 0});
dis[s][0] = 0, cnt[s][0] = 1;
while (!q.empty()) {
Node qt = q.top(); q.pop();
int u = qt.x, Dist = qt.dis, Type = qt.type, Count = cnt[u][Type];
if (vis[u][Type]) continue;
vis[u][Type] = 1;
for (int v : G[u]) {
if (dis[v][0] > Dist + 1) {
dis[v][1] = dis[v][0], cnt[v][1] = cnt[v][0];
q.push((Node){v, dis[v][1], 1});
dis[v][0] = Dist + 1, cnt[v][0] = Count;
q.push((Node){v, dis[v][0], 0});
}
else if (dis[v][0] == Dist + 1)
cnt[v][0] = (cnt[v][0] + Count) % mod;
else if (dis[v][1] > Dist + 1) {
dis[v][1] = Dist + 1, cnt[v][1] = Count;
q.push((Node){v, dis[v][1], 1});
}
else if (dis[v][1] == Dist + 1)
cnt[v][1] = (cnt[v][1] + Count) % mod;
}
}
int res = cnt[t][0];
if (dis[t][1] == dis[t][0] + 1) res = (res + cnt[t][1]) % mod;
return res;
}
int main()
{
int T;
cin >> T;
while (T--) cout << solve() << endl;
return 0;
}