2022牛客冬令营 第二场 题解
A题 小沙的炉石(数学,思维)
现在我们有 \(n\) 张攻击牌和 \(m\) 张回蓝牌。攻击牌消耗一格蓝量,并且对对方造成 1 点普通伤害和附带的法伤。回蓝牌则回复一格蓝量(蓝量无上限)。
此外,我们每使用一张牌之后,可以使得我们的法力伤害值增加 1,可叠加(初始值为 0)。
现在有 \(k\) 次询问,每次给定敌方血量 \(x\),问我们能否找到一个出牌顺序,使得地方能够恰好被我们斩杀(不要求用完牌)。
\(1\leq n\leq 10^9,0\leq m \leq 10^9,1\leq k\leq 10^5\),看不懂法伤那部分可以看样例。
我们不难找到伤害最大化的方法并计算出来:前面疯狂堆回复牌,然后后面能打多少进攻牌就打多少,值的话就直接等差数列套个公式就行。
接下来,我们看看我们可以构造出哪些攻击方式。
在进行 \(t\) 次攻击的情况下,我们至少可以造成 \(t^2\) 的伤害(1010101这种方式),至多造成 \(\frac{t(2m+t+1)}{2}\) 的伤害。值得注意的是,这是连续的(临项交换法,将攻击牌往后面交换位置)。
不过显然,我们不能每次一个个求出来区间,然后每次询问的时候都去遍历一遍,那么我们只能看看这些区间能不能合并(显然是可以的)
实践发现,仅存在 \(t=2,m=1,x=3\) 和 \(t=3,m=2,b=8\) 两种情况是特例(可以自己草稿纸上面推,也可以写个暴力直接莽),其余情况下直接看在不在 \([1,Max]\) 里面即可。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
int main()
{
LL n, m, k;
cin >> n >> m >> k;
LL t = min(n, m + 1), Max = t * (2 * m + t + 1) / 2;
while (k--) {
LL x;
cin >> x;
if ((t == 2 && m == 1 && x == 3) || (t == 3 && m == 2 && x == 8))
puts("NO");
else
puts(x > Max ? "NO" : "YES");
}
return 0;
}
B题 小沙的魔法(排序,离散化,图上遍历)
给定一个 \(n\) 个点的图,每个点有一个权值 \(a_i\)。现在我们每次可以进行一次操作,使得某个点的权值下降 1,我们的目标是使得所有点的权值变为 0。
不过,单纯这样操作有点慢,所以我们还有 \(m\) 个连接器,每个连接器可以将两个点进行连接,之后每次操作可以视为对整个连通块进行一次操作。连接器至多使用 \(\min(5n,m)\) 次。
\(1\leq n \leq 5*10^5,1\leq m \leq \min(\frac{n(n-1)}{2},5*10^6),0\leq a_i\leq 10^9\)
(至多 \(n-1\) 次连接就可以使得整个图联通,所以连接器的次数限制无意义。)
这题其实和著名的 P1969 [NOIP2013 提高组] 积木大赛 很类似,不过那题是线性,本题搬到了图上面。
先排个序,然后离散化一下,从大到小依次处理。我勉强看得懂题解代码和思路,但有一说一,自己真写不出来。
#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 500010;
int n, m, s[N];
vector<int> e[N], ans[N], mp;
int F(int x) { return lower_bound(mp.begin(), mp.end(), x) - mp.begin(); }
//
int fa[N];
int find(int x) {
if (x != fa[x]) fa[x] = find(fa[x]);
return fa[x];
}
bool merge(int x, int y) {
x = find(x), y = find(y);
if (x != y) fa[x] = y;
return x != y;
}
int main()
{
scanf("%d%d", &n, &m);
mp.resize(n + 1, 0);
for (int i = 1; i <= n; i++) {
scanf("%d", &s[i]);
mp[i] = s[i];
}
//离散化
sort(mp.begin(), mp.end());
mp.erase(unique(mp.begin(), mp.end()), mp.end());
for (int i = 1; i <= n; i++)
ans[F(s[i])].push_back(i);
//图的构建
for (int i = 1; i <= m; i++) {
int u, v;
scanf("%d%d", &u, &v);
e[u].push_back(v), e[v].push_back(u);
}
//solve
for (int i = 1; i <= n; ++i) fa[i] = i;
LL res = 0, cnt = 0;
for (int i = mp.size() - 1; i >= 1; i--) {
for (int x : ans[i]) {
cnt++;
for (int y : e[x])
if (s[y] >= s[x])
if (merge(x, y)) cnt--;
}
res += (mp[i] - mp[i - 1]) * cnt;
}
//output
printf("%lld", res);
return 0;
}
C题 小沙的杀球(贪心)
小沙特别喜欢杀球(羽毛球的一个玩法),但是水平有限,只能杀后场的高远球,杀不了前场的小球。
小沙是有体力限制的,每杀一个球就会消耗 \(a\) 体力,但是不杀球的话则会回复 \(b\) 体力。体力没有上限,但是体力不可能为负。
现在小沙有体力 \(x\),现在即将有 \(n\) 个球打过来,并且知道他们分别是哪种球(按顺序),试求出小沙究竟最多能杀多少球?
\(0\leq x,a,b\leq 10^9,1\leq n\leq 10^6\)
按照贪心,尽可能的来杀高远球,如果体力不够或者是小球就休息一下,恢复体力。
这题会卡 int,所以记得开 long long。
#include<bits/stdc++.h>
using namespace std;
const int N = 1000010;
int n;
char s[N];
int main()
{
long long x, a, b;
scanf("%lld%lld%lld%s", &x, &a, &b, s + 1);
n = strlen(s + 1);
int ans = 0;
for (int i = 1; i <= n; ++i)
if (s[i] == '1' && x >= a)x -= a, ans++;
else x += b;
printf("%d", ans);
return 0;
}
D题 小沙的涂色(unsolved)
小思维
细节
中模拟
构造
有许多种构造思路,这里列举出一种
如果边长是3的倍数我们便无法构造出合法方案。因为我们需要填色的格子数一定是3的倍数,所以要满足 \((n^2-1)=0 \mod 3\)
对于边长mod 3情况下是1的情况,我们可以选择试边界向内缩小6格的方案使他一直维持 mod 3 为 1,最后只剩一个格子停下即可,对于边长 mod 3 为 2 的情况,我们需要让他变成 1,所以我们可以选择向内缩小 4格的方案,从外向内染色。
也可以使用二分的方法染色,解法很多,只要你觉得好写~
时间复杂度\(O(n^2)\)
E题 小沙的长路(图论推理)
给定一个 \(n\) 个点的完全图,现在我们可以任意给这些边加方向,问加上之后这个图的最长路的可能的最小值和最大值。
\(1\leq n \leq 10^9\)
-
最大值
当 \(n\) 为奇数的时候,直接把图当成一个无向图来走欧拉通路即可(每个点的度都是 \(n-1\),偶数,所以存在欧拉通路),然后按照路径来标边的方向,值为 \(\frac{n(n-1)}{2}\)。
当 \(n\) 为偶数的时候,所有点的度数都是奇数,不存在欧拉通路。如果我们将最长路之外的边删掉,意味着这条最长路就是剩下的边所组成的图的欧拉通路。那显然,删掉 \(\frac{n}{2}-1\) 条边的时候,可以将 \(n-2\) 个点的度数减去一(变成偶数),再留下两个奇数点,满足构成欧拉回路的条件,所以答案为 \(\frac{n(n-1)}{2}-(\frac{n}{2}-1)\)(也是删边最少的方案)
-
最小值
我们逐个添加点,发现每添加一个点,最长路径至少增加 1(可以根据每个点的入度出度来判断)。
由数学归纳法可知,最小值为 \(n-1\) 。
#include<bits/stdc++.h>
using namespace std;
int main()
{
long long n;
cin >> n;
printf("%lld %lld", n - 1, n * (n - 1) / 2 - (n % 2 ? 0 : n / 2 - 1));
return 0;
}
F题 小沙的算数 (模拟,逆元)
现在有一个长度为 \(n\),仅含加号和乘号的表达式,每个位置上面有一个初始值。
现在,有 \(q\) 次询问,每次询问会要求将位置 \(x\) 上面的数改成 \(y\),然后计算一下表达式的值并且输出。
\(n\leq 10^6,q\leq 10^5\),答案对 \(10^9+7\) 取模且题目中给出的所有数值都在 \([1,10^9+7)\) 范围内。
仅包含加号和乘号,那我们可以将不同位置的数,打上不同的标记(相同标记表示在一个连通块内),一个连通块的值等于等于该标记的值的乘积,表达式的值等于所有连通块之和。
对于每次询问,我们找到位置 \(x\) 对应的连通块,然后就是一连串的先除再乘,顺便更新答案即可。因为取模的原因,所以还要写下逆元啥的。
#include<bits/stdc++.h>
using namespace std;
//
#define LL long long
const int N = 1000010;
const LL mod = 1000000007;
LL power(LL a, LL b) {
a %= mod;
LL res = 1;
while (b) {
if (b & 1) res = res * a % mod;
b >>= 1;
a = a * a % mod;
}
return res;
}
LL inv(LL x) {
return power(x, mod - 2);
}
//
int n, q;
char opt[N];
int vis[N];
LL a[N], v[N];
int main()
{
scanf("%d%d", &n, &q);
scanf("%s", opt + 1);
//
vis[1] = 1;
for (int i = 1; i < n; ++i)
vis[i + 1] = vis[i] + (opt[i] == '+');
//
for (int i = 1; i <= n; ++i)
scanf("%lld", &a[i]);
for (int i = 1; i <= vis[n]; ++i) v[i] = 1;
for (int i = 1; i <= n; ++i)
v[vis[i]] = v[vis[i]] * a[i] % mod;
//
LL ans = 0;
for (int i = 1; i <= vis[n]; ++i)
ans += v[i];
while (q--) {
int x;
LL y;
scanf("%d%lld", &x, &y);
int tag = vis[x];
ans = (ans - v[tag] + mod) % mod;
v[tag] = v[tag] * inv(a[x]) % mod * y % mod;
ans = (ans + v[tag]) % mod;
a[x] = y;
printf("%lld\n", ans);
}
return 0;
}
G题 小沙的身法(LCA)
给定一个有 \(n\) 个点的联通树,每个点有一个高度 \(a_i\)。
我们可以沿着树上面的边移动,从 \(x\) 跳到 \(y\),需要花费 \(\max(a_y-a_x,0)\) 的体力(从高处往低处跳不需要体力,反之则需要耗费高度差的体力)。
现在有 \(m\) 次询问,每次询问给定起点和终点,我们从地上跳到起点,然后一路跳到终点后跳回地上,问需要耗费的体力是多少?(地面可以视为高度为 0,但是只有开头和结束可以跳)
\(1\leq n \leq 10^6,1\leq m\leq 10^5,1\leq a_i\leq 10^9\)
树上路径,多次查询,那显然就是倍增 LCA了。
考虑到具有方向性,所以我们需要构建两颗树,均以 1 为根,然后一个按照从深度低往深度高的方式赋边权,另一棵树反过来,然后每次询问的时候分别查询即可。
#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 1000010;
int lg[N];
//
int n, m, a[N];
//
vector<int> tree[N];
int depth[N], fa[N][23];
LL dis1[N], dis2[N];
void dfs(int x, int f) {
depth[x] = depth[f] + 1;
fa[x][0] = f;
for (int i = 1; (1 << i) <= depth[x]; i++)
fa[x][i] = fa[fa[x][i - 1]][i - 1];
for (int y : tree[x])
if (y != f) {
dis1[y] = dis1[x] + max(a[x] - a[y], 0);
dis2[y] = dis2[x] + max(a[y] - a[x], 0);
dfs(y, x);
}
}
int lca(int x, int y) {
if (depth[x] < depth[y]) swap(x, y);
while (depth[x] > depth[y])
x = fa[x][lg[depth[x] - depth[y]]];
if (x == y) return x;
for (int k = lg[depth[x]]; k >= 0; k--)
if (fa[x][k] != fa[y][k])
x = fa[x][k], y = fa[y][k];
return fa[x][0];
}
int main()
{
lg[1] = 0;
for (int i = 2; i < N; i++)
lg[i] = lg[i / 2] + 1;
//read & build
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
tree[u].push_back(v);
tree[v].push_back(u);
}
dfs(1, 0);
while (m--) {
int u, v;
scanf("%d%d", &u, &v);
int x = lca(u, v);
printf("%lld\n", a[u] + (dis1[u] - dis1[x]) + (dis2[v] - dis2[x]));
}
return 0;
}
H题 小沙的数数(数学)
已知一个长度为 \(n\) 的非负整数数列 \({a_n}\),数值和为 \(m\)。
现在我们想要让数列的异或和最大,尝试找出有几种方案。
\(1\leq n \leq 10^{18},0\leq a_i,m\leq 10^{18}\),方案数对 \(10^9+7\) 取模。
不是很好证明,异或和的最大值就是 \(m\):感性上讲,就是讲 \(m\) 的二进制上面的 1,被均匀分配到 \(n\) 个坐标的不同位置上面,能够使得异或和最大。而任何尝试使异或值变得更大的方式,都会使得数值和超过 \(m\)。
分配方式很奇妙:我们记 \(m\) 在二进制下有 \(t\) 个 1,那么每个位置的 1 都有 \(n\) 个选择,所以答案是 \(n^t\)。
\(n\) 的规模比较离谱,所以一开始就得取个模,不然直接炸 long long。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const LL mod = 1e9 + 7;
int main()
{
LL n, m, ans = 1;
cin >> n >> m;
n %= mod;
for (; m; m >>= 1)
if (m % 2) ans = ans * n % mod;
cout << ans;
return 0;
}
I题 小沙的构造(模拟,构造)
直接看代码,别问咋构造的,我 WA 了 12 发才过就离谱。
#include<bits/stdc++.h>
#include<bits/stdc++.h>
using namespace std;
string str1 = "\"!'*+-.08:=^_WTYUIOAHXVM|";//0 1 2 ... 24
string str2 = "<>\\/[]{}()";// 01 23 45 67 89
int n, m;
string solve() {
deque<char> q1, q2, q3;
int cnt = 0, len = 0;
for (int i = 0; i < 5 && cnt + 2 < m; ++i) {
q1.push_front(str2[2 * i]);
q3.push_back (str2[2 * i + 1]);
cnt += 2, len += 2;
}
for (int i = 0; i < 24 && cnt + 1 < m; ++i) {
q1.push_front(str1[i]);
q3.push_back (str1[i]);
cnt += 1, len += 2;
}
if (n - len <= 0) return "-1";
for (int i = 0; i < n - len; ++i)
q2.push_back(str1[24]);
string res = "";
for (char c : q1) res.push_back(c);
for (char c : q2) res.push_back(c);
for (char c : q3) res.push_back(c);
return res;
}
int main()
{
//read
cin >> n >> m;
//solve
string ans;
if (m == 36 || (m > 11 && n < 2 * m - 11) || (m <= 11 && n < m)) ans = "-1";
else ans = solve();
//solve
cout << ans;
return 0;
}
J题 小沙的Dota(unsolved)
DDP(所需知识点线段树)
考虑转移方程 \(dp_{i,v}=\min(dp_{i,v},dp_{i-1,x})\)
在需要修改的情况下,我们可以采用线段树维护带修改的 DP 过程。
对于节点维护左状态为 V 的情况下变化到 X 所需要的最小代价,预处理各个状态之间的代价转移即可
时间复杂度:\(O(6^3(n+m)\log n)\)
\(O(6^4(n+m)\log n)\)的合并写丑了可能会被卡。
K题 小沙的步伐 (签到)
除了在 5 上面不用动,别的地方直接都是目标位置和 5 都加 1。
#include<bits/stdc++.h>
using namespace std;
int ans[10];
int main()
{
string str;
cin >> str;
for (char c : str)
if (c != '5') ans[c - '0']++, ans[5]++;
for (int i = 1; i < 10; ++i)
printf("%d ", ans[i]);
return 0;
}
L/M题 小沙的remake(DP,树状数组,排序)
不难写出一个 \(O(n^2)\) 的 DP:
for (int i = 1; i <= n; ++i) {
dp[i] = 1;
for (int j = max(i - b[i], 1); j < i; ++j)
if (a[i] >= a[j])
dp[i] = (dp[i] + dp[j]) % mod;
}
//output
LL ans = 0;
for (int i = 1; i <= n; ++i)
ans = (ans + dp[i]) % mod;
printf("%lld", ans);
这题是最长上升子序列的一种变形(从最长变成了求方案数,同时加上了范围限制),以前是可以通过树状数组+离散化来优化复杂度,但是对这题似乎不太够。
这题比较特殊,求方案数,所以我们在传统下标上面建立树状数组(用来求和),但是遍历DP的时候按照排序过后的下标来,这样恰好满足了:
- 可以限定下标范围(显然的,就相当于上面那个朴素DP的优化)
- 根据按照数值排序后的下标顺序来进行状态转移,保证了每次查询之后的值都是合法的,可以直接求和
#include <bits/stdc++.h>
#define LL long long
namespace GenHelper
{
int z1, z2, z3, z4, z5, u, res;
int get()
{
z5 = ((z1 << 6) ^ z1) >> 13;
z1 = ((int)(z1 & 4294967) << 18) ^ z5;
z5 = ((z2 << 2) ^ z2) >> 27;
z2 = ((z2 & 4294968) << 2) ^ z5;
z5 = ((z3 << 13) ^ z3) >> 21;
z3 = ((z3 & 4294967) << 7) ^ z5;
z5 = ((z4 << 3) ^ z4) >> 12;
z4 = ((z4 & 4294967) << 13) ^ z5;
return (z1 ^ z2 ^ z3 ^ z4);
}
int read(int m)
{
u = get();
u >>= 1;
if (m == 0)
res = u;
else
res = (u / 2345 + 1000054321) % m;
return res;
}
void srand(int x)
{
z1 = x;
z2 = (~x) ^ (0x23333333);
z3 = x ^ (0x12345798);
z4 = (~x) + 51;
u = 0;
}
}
using namespace GenHelper;
using namespace std;
const int N = 2e6 + 7, mod = 1e9 + 7;
int n, seed;
int a[N], b[N];
pair<int, int> p[N];
//TreeArray
#define lowbit(x) (x & (-x))
LL t[N];
LL query(int x) {
LL res = 0;
for (; x; x -= lowbit(x))
res = (res + t[x]) % mod;
return res;
}
void add(int x, int val) {
for (; x <= n; x += lowbit(x))
t[x] = (t[x] + val) % mod;
}
//Main
int main()
{
scanf("%d %d", &n, &seed);
srand(seed);
for (int i = 1; i <= n; i++) {
a[i] = read(0), b[i] = read(i);
p[i] = {a[i], i};
}
sort(p + 1, p + n + 1);
LL ans = 0;
for (int i = 1; i <= n; i++) {
int id = p[i].second;
LL val = (query(id) - query(max(0, id - b[id] - 1)) + 1 + mod) % mod;
add(id, val);
ans = (ans + val) % mod;
}
cout << ans << endl;
return 0;
}