【题解】2021牛客暑期多校训练营2
A
先考虑怎么判断区间 排序后是否能构成等差数列。
假设最后构成等差为 的等差数列
,充要条件就是:
。
如果最后能构成等差数列,就可以将数列 中每个元素减去
(这样仍是等差数列),然后除以次小值(最小值一定是
),得到的一定是
的排列。否则,一定不能构成等差数列。
考虑第二个条件,可以用辗转相除法变成 ,设答案为
, 则两两的差一定是
的倍数。
问题就转化为 。
因为 ,所以用线段树维护这个值的最小值以及最小值个数。
每次移动右端点,可以用单调栈来维护极值,每个数只会弹出一次。若固定某个点为区间左端点,那么随着右端点的移动, 最多只会变化
次,因为每次减至少除以
。对于每一个右端点,从右往左枚举左端点更新
,一旦更新不了,就可以
了。然后会有
段不同的
,对着
个区间减去相应的
。总时间复杂度是
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 9;
int read() {
int f = 0, x = 0;
char ch = getchar();
while (!isdigit(ch)) {
f |= (ch == '-');
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + ch - '0';
ch = getchar();
}
return f ? -x : x;
}
int t, n, w[N];
long long ans;
int mn[N << 2], cnt[N << 2], tag[N << 2], d[N << 2];
int sa[N], ta;
int sb[N], tb;
void push_down(int p) {
tag[p << 1] += tag[p];
mn[p << 1] += tag[p];
tag[p << 1 | 1] += tag[p];
mn[p << 1 | 1] += tag[p];
tag[p] = 0;
}
void push_up(int p) {
cnt[p] = 0;
mn[p] = min(mn[p << 1], mn[p << 1 | 1]);
if (mn[p] == mn[p << 1])
cnt[p] += cnt[p << 1];
if (mn[p] == mn[p << 1 | 1])
cnt[p] += cnt[p << 1 | 1];
d[p] = (d[p << 1] == d[p << 1 | 1] ? d[p << 1] : 0);
}
void build(int p, int l, int r) {
tag[p] = 0;
mn[p] = 0;
d[p] = 0;
cnt[p] = r - l + 1;
if (l == r)
return;
int mid = (l + r) >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
}
void add(int p, int l, int r, int nl, int nr, int v) {
if (nl <= l && r <= nr) {
mn[p] += v;
tag[p] += v;
return;
}
int mid = (l + r) >> 1;
push_down(p);
if (nl <= mid)
add(p << 1, l, mid, nl, nr, v);
if (nr > mid)
add(p << 1 | 1, mid + 1, r, nl, nr, v);
push_up(p);
}
void change(int p, int l, int r, int nl, int nr, int v) {
if (nl <= l && r <= nr && d[p] && v % d[p] == 0) {
mn[p] -= d[p];
tag[p] -= d[p];
return;
}
if (l == r) {
mn[p] += (nr - r) * d[p];
d[p] = __gcd(d[p], v);
mn[p] -= (nr - r + 1) * d[p];
return;
}
int mid = (l + r) >> 1;
push_down(p);
if (nl <= mid)
change(p << 1, l, mid, nl, nr, v);
if (nr > mid)
change(p << 1 | 1, mid + 1, r, nl, nr, v);
push_up(p);
}
int get(int p, int l, int r, int nl, int nr) {
if (nl <= l && r <= nr)
return (mn[p] == 0) * cnt[p];
int mid = (l + r) >> 1;
int res = 0;
push_down(p);
if (nl <= mid)
res += get(p << 1, l, mid, nl, nr);
if (nr > mid)
res += get(p << 1 | 1, mid + 1, r, nl, nr);
return res;
}
signed main() {
t = read();
while (t--) {
scanf("%d", &n);
ans = 0;
for (int i = 1; i <= n; ++i) scanf("%d", &w[i]);
build(1, 1, n);
ta = tb = 0;
for (int i = 1; i <= n; ++i) {
while (ta && w[sa[ta]] < w[i]) {
add(1, 1, n, sa[ta - 1] + 1, sa[ta], -w[sa[ta]]);
ta--;
}
while (tb && w[sb[tb]] > w[i]) {
add(1, 1, n, sb[tb - 1] + 1, sb[tb], w[sb[tb]]);
tb--;
}
sa[++ta] = i;
add(1, 1, n, sa[ta - 1] + 1, sa[ta], w[sa[ta]]);
sb[++tb] = i;
add(1, 1, n, sb[tb - 1] + 1, sb[tb], -w[sb[tb]]);
if (i != 1)
change(1, 1, n, 1, i - 1, abs(w[i] - w[i - 1]));
ans += get(1, 1, n, 1, i);
}
printf("%lld\n", ans);
}
return 0;
}
B
在 个炮的一行中吃一次方案数为
,因为有方向。
所以吃 次的方案数就是
为了方便
第一种情况对于某个次数 方案数就是
然后在第一行选择 次,第二行选择
次。
考虑后面一部分的组合意义,就是从 个数里,有序地选择
个数。那么可以化简为
所以第一种方案数就是 ,可以直接递推
第二种情况的方案数就是
$$
最后一步是换成了枚举 。然后就是组合数的前缀和问题。
$$
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int p = 1e9 + 9;
const int N = 1e7 + 5;
int x, y;
int fac[N], ifac[N], sum[N];
int inv(int x, int base = p - 2) {
int res = 1;
while (base) {
if (base & 1)
res = res * x % p;
x = x * x % p;
base >>= 1;
}
return res;
}
int C(int n, int m) { return n >= m && m >= 0 ? fac[n] * ifac[m] % p * ifac[n - m] % p : 0; }
void init(int n = N - 1) {
fac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % p;
ifac[n] = inv(fac[n]);
for (int i = n - 1; i >= 0; i--) ifac[i] = ifac[i + 1] * (i + 1) % p;
assert(fac[10] * ifac[10] % p == 1);
}
signed main() {
scanf("%lld%lld", &x, &y);
x -= 2;
y -= 2;
init();
int ans1 = 0, ans2 = 0;
int p2 = 1;
sum[0] = 1;
for (int i = 1; i <= x + y; ++i) {
sum[i] = sum[i - 1] * 2 % p;
sum[i] = ((sum[i] - C(i - 1, x) - C(i - 1, y)) % p + p) % p;
}
for (int i = 0; i <= x + y; ++i) {
int res = p2 * fac[i] % p * C(x + y, i) % p;
ans1 ^= res;
res = p2 * fac[x] % p * fac[y] % p * ifac[x + y - i] % p * sum[x + y - i] % p;
ans2 ^= res;
p2 = p2 * 2 % p;
}
printf("%lld %lld\n", ans1, ans2);
return 0;
}
C
首先有结论若 则先手必胜,否则后手必胜。证明如下:
若 ,则先手无论选择哪一条边
,后手都以被选过一次的
为起点,走向另一个从来没被连过的点
。这样
次就会覆盖所有
个点。下次一定会连接两个被选过的点,从而形成闭环。否则,先手随便走一步,就变成了
的局面了。
#include <bits/stdc++.h>
int main() {
int n, m;
scanf("%d%d", &n, &m);
printf("%s\n", (n * m) & 1 ? "NO" : "YES");
return 0;
}D
直接按照题意模拟即可。也可以把每一个特征赋一个权,最后就可以直接比较,减少分类讨论。
#include <bits/stdc++.h>
#define int long long
using namespace std;
signed main() {
int T;
int a1, a2, b1, b2;
int va = 0, vb = 0;
cin >> T;
for (int i = 1; i <= T; i++) {
cin >> a1 >> a2 >> b1 >> b2;
if (a1 > a2)
swap(a1, a2);
if (b1 > b2)
swap(b1, b2);
if (a1 == 2 && a2 == 8)
va += 10000;
if (b1 == 2 && b2 == 8)
vb += 10000;
if (a1 == a2)
va += 1000 * a1;
if (b1 == b2)
vb += 1000 * b1;
if (a1 != a2)
va += 100 * ((a1 + a2) % 10) + a2;
if (b1 != b2)
vb += 100 * ((b1 + b2) % 10) + b2;
if (va > vb)
cout << "first" << endl;
if (va < vb)
cout << "second" << endl;
if (va == vb)
cout << "tie" << endl;
va = 0;
vb = 0;
}
return 0;
}F
假设某个人在点 ,另外两个人坐标分别是
,则:
$$
发现和球的方程非常类似。
一般方程:
标准方程:
所以球心坐标为
然后计算两个球相交部分体积即可。
#include <bits/stdc++.h>
using namespace std;
const double pi = acos(-1.0);
const double eps = 1e-10;
struct point {
double x, y, z;
} a, b, c, d;
double dis(point p, point q) {
return sqrt((p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y) + (p.z - q.z) * (p.z - q.z));
}
int t, k1, k2;
int main() {
scanf("%d", &t);
while (t--) {
scanf("%lf%lf%lf", &a.x, &a.y, &a.z);
scanf("%lf%lf%lf", &b.x, &b.y, &b.z);
scanf("%lf%lf%lf", &c.x, &c.y, &c.z);
scanf("%lf%lf%lf", &d.x, &d.y, &d.z);
scanf("%d%d", &k1, &k2);
double d1 = dis(a, b);
double d2 = dis(c, d);
double r1 = d1 / (k1 * k1 - 1) * k1, r2 = d2 / (k2 * k2 - 1) * k2;
point o1, o2;
o1.x = (b.x * k1 * k1 - a.x) / (k1 * k1 - 1);
o1.y = (b.y * k1 * k1 - a.y) / (k1 * k1 - 1);
o1.z = (b.z * k1 * k1 - a.z) / (k1 * k1 - 1);
o2.x = (d.x * k2 * k2 - c.x) / (k2 * k2 - 1);
o2.y = (d.y * k2 * k2 - c.y) / (k2 * k2 - 1);
o2.z = (d.z * k2 * k2 - c.z) / (k2 * k2 - 1);
double d = dis(o1, o2);
if (d - r1 - r2 > eps) {
puts("0.0000");
continue;
}
if (r1 < r2) {
swap(r1, r2);
swap(o1, o2);
}
if (r1 - r2 - d > eps)
printf("%.4f\n", 4 * pi / 3 * r2 * r2 * r2);
else {
double ca = (r1 * r1 + d * d - r2 * r2) / r1 / d / 2, h1 = r1 * (1.0 - ca);
double cb = (r2 * r2 + d * d - r1 * r1) / r2 / d / 2, h2 = r2 * (1.0 - cb);
printf("%.4f\n", pi / 3 * ((3 * r1 - h1) * h1 * h1 + (3 * r2 - h2) * h2 * h2));
}
}
return 0;
}G
如果存在两个区间 满足
,即大区间包含小区间,那么这个大区间相当于没有额外贡献,把他提出来单独成一个区间有可能更优。注意,不可能将两个及以上的“大区间”分成一组,否则留下一个,把剩下的分配到相应的小区间所在组会更优。
剩下来的小区间按照左端点排序,可知右端点也会单调递增(不然就会存在包含关系,而已经把把包含关系中的大区间筛去了)。设 为前
组用了前
个线段的最优答案。那么就能得到一个时间复杂度为
的算法。
观察 转移式子
$$
把 提出来后,就发现是经典的可以用单调队列来解决的转移方程。具体的,只需要从小到大枚举
然后
从大到小避免后效性,对于每一个
都维护一个单调队列即可,维护
的最大值,每次把队尾
的
都弹掉,这样总时间复杂度是
.
最后枚举这些小区间分成多少组,剩下的用大区间来补全统计答案。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 9;
struct node {
int l, r;
} a[N], b[N];
int dp[N][N], q[N], c[N];
int n, x, y, z, ans, sum, k;
bool cmp(node A, node B) {
if (A.l == B.l)
return A.r > B.r;
return A.l < B.l;
}
int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++) scanf("%d%d", &a[i].l, &a[i].r);
sort(a + 1, a + 1 + n, cmp);
int aaa = 1000000000, top = 0, m = 0;
for (int i = n; i >= 1; i--) {
if (a[i].r >= aaa)
c[++top] = a[i].r - a[i].l;
else {
aaa = a[i].r;
b[++m] = a[i];
}
}
memset(dp, -1, sizeof(dp));
sort(c + 1, c + 1 + top, greater<int>());
reverse(b + 1, b + 1 + m);
dp[0][0] = 0;
for (int i = 1; i <= k; i++) {
int sl = 1, sr = 0;
for (int j = 1; j <= m; j++) {
if (dp[i - 1][j - 1] != -1) {
while (sl <= sr && dp[i - 1][q[sr]] + b[q[sr] + 1].r <= dp[i - 1][j - 1] + b[j].r) sr--;
q[++sr] = j - 1;
}
while (sl <= sr && b[q[sl] + 1].r <= b[j].l) sl++;
if (sl <= sr)
dp[i][j] = dp[i - 1][q[sl]] + b[q[sl] + 1].r - b[j].l;
}
}
for (int i = 0; i <= min(k, n); i++) {
sum += c[i];
if (dp[k - i][m] != -1)
ans = max(ans, dp[k - i][m] + sum);
}
cout << ans;
return 0;
}I
状态记录两只企鹅的位置状态,保存上一步的位置,直接广搜,然后根据路径构造答案。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 9;
const int M = 30;
const int inf = 0x3f3f3f3f;
char s1[M][M], s2[M][M], sta[N * 10];
char c[4] = { 'D', 'L', 'R', 'U' };
int top;
struct node {
int x1, y1, x2, y2;
};
int d[M][M][M][M], p[M][M][M][M];
node pre[M][M][M][M];
int dir[4][4] = { 1, 0, 1, 0, 0, -1, 0, 1, 0, 1, 0, -1, -1, 0, -1, 0 };
queue<node> q;
void bfs() {
memset(d, inf, sizeof(d));
d[20][20][20][1] = 0;
q.push(node{ 20, 20, 20, 1 });
while (!q.empty()) {
node now = q.front();
q.pop();
int x1 = now.x1, y1 = now.y1, x2 = now.x2, y2 = now.y2;
int dis = d[x1][y1][x2][y2];
for (int i = 0; i < 4; i++) {
int u1 = x1 + dir[i][0], v1 = y1 + dir[i][1], u2 = x2 + dir[i][2], v2 = y2 + dir[i][3];
if (s1[u1][v1] != '.')
u1 -= dir[i][0], v1 -= dir[i][1];
if (s2[u2][v2] != '.')
u2 -= dir[i][2], v2 -= dir[i][3];
if (dis + 1 < d[u1][v1][u2][v2]) {
d[u1][v1][u2][v2] = dis + 1;
pre[u1][v1][u2][v2] = node{ x1, y1, x2, y2 };
p[u1][v1][u2][v2] = i;
q.push(node{ u1, v1, u2, v2 });
}
}
}
}
void solve(node u) {
int x1 = u.x1, y1 = u.y1, x2 = u.x2, y2 = u.y2;
s1[x1][y1] = s2[x2][y2] = 'A';
if (x1 == 20 && y1 == 20 && x2 == 20 && y2 == 1)
return;
solve(pre[x1][y1][x2][y2]);
sta[top++] = c[p[x1][y1][x2][y2]];
}
signed main() {
for (int i = 1; i <= 20; i++) scanf("%s%s", s1[i] + 1, s2[i] + 1);
bfs();
solve(node{ 1, 20, 1, 1 });
printf("%d\n", top);
printf("%s\n", sta);
for (int i = 1; i <= 20; i++) printf("%s %s\n", s1[i] + 1, s2[i] + 1);
}J
枚举 ,然后求有多少个大小为
的子集
为
枚举 的倍数,若为
个,方案数就为
。但是这样会包含
是
的倍数的方案,只需要枚举倍数,减掉他就可以了。设求得的答案为
最终的答案就是
很大,但根据扩展欧拉定理:
$a_d
\phi(p)
\rm int128$ ,或者龟速乘。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 80000;
int vis[80005], pr[80005], len;
int T, n, k, a[80005], res, b[35];
int p, ans;
int mul(int a, int b) {
int s = (long double)a / p * b, t = a * b - s * p;
return t < 0 ? t + p : t;
}
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
int inv(int x, int base = p - 2) {
int res = 1;
while (base) {
if (base & 1)
res = mul(res, x);
base >>= 1, x = mul(x, x);
}
return res;
}
int calc(int N, int m, int t) {
for (int i = 1; i <= m; ++i) b[i] = N - i + 1;
for (int i = 2; i <= m; ++i) {
int now = i;
for (int j = 1, g; now > 1 && j <= m; ++j) {
g = gcd(b[j], now);
b[j] /= g, now /= g;
}
}
int res = inv(t, b[1]);
for (int i = 2; i <= m; ++i) res = inv(res, b[i]);
return res;
}
void init() {
for (int i = 2; i <= N; ++i) {
if (!vis[i])
pr[++len] = i;
for (int j = 1; j <= len && i * pr[j] <= N; ++j) {
vis[i * pr[j]] = 1;
if (i % pr[j] == 0)
break;
}
}
}
signed main() {
init();
scanf("%d", &T);
while (T--) {
cin >> n >> k >> p;
memset(a, 0, sizeof(a));
for (int i = 1, x; i <= n; ++i) {
cin >> x;
a[x]++;
}
ans = 1;
for (int i = 1; i <= len; ++i) {
for (int now = pr[i]; now <= N; now *= pr[i]) {
res = 0;
for (int j = now; j <= N; j += now) res += a[j];
if (res < k)
continue;
ans = mul(ans, calc(res, k, pr[i]));
}
}
cout << ans << endl;
}
return 0;
}K
我们根据单调栈的操作来连边。具体的,如果 令
弹出,那么
表示
。否则
表示
然后做一遍拓扑排序,按照遍历的顺序赋值一定满足题目要求。如果图不是一张 ,说明有环无解,直接输出
。
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 9;
int n, k;
PII p[N];
int a[N], b[N], to[N], d[N], sta[N], q[N];
bool check() {
for (int i = 0; i < k; i++) {
int pos = p[i].first, val = p[i].second;
int pp = p[i + 1].first, vv = p[i + 1].second;
if (pos < val)
return true;
if (i == k - 1)
break;
if (val + pp - pos < vv)
return true;
}
return false;
}
void topsort() {
int l = 0, r = -1;
for (int i = 1; i <= n; i++)
if (!d[i])
q[++r] = i;
while (l <= r) {
int x = q[l++];
d[to[x]]--;
if (d[to[x]] == 0)
q[++r] = to[x];
}
int x = n;
for (int i = 0; i <= r; i++) {
int pos = q[i];
a[pos] = x--;
}
for (int i = 1; i <= n; i++) cout << a[i] << " \n"[i == n];
}
void solve() {
int top = 0;
for (int i = 1; i <= n; i++) {
if (b[i]) {
bool flag = false;
while (top > b[i] - 1) top--, flag = true;
if (flag)
to[sta[top + 1]] = i;
}
sta[++top] = i;
to[i] = sta[top - 1];
}
for (int i = 1; i <= n; i++) d[to[i]]++;
topsort();
}
int main() {
cin >> n >> k;
for (int i = 0; i < k; i++) {
int pos, val;
cin >> pos >> val;
p[i] = { pos, val };
b[pos] = val;
}
if (check())
puts("-1");
else
solve();
return 0;
}L
注意到两个信息
- 每次只改一个点权值
- 最大权值小于等于
按照 对每个点的度数进行分治,度数大于
的点称为大点,剩下的称为小点。
- 修改小点的时候,可以暴力遍历与它相邻的点,并维护相应的答案。
- 对于大点,先暴力遍历与他相邻的大点,至多有
个点。但是却不能枚举小点。这时候就要利用权值不超过
的性质了。假设更改前点权为
,更改后为
,那么会改变夺冠情况的只有点权位于
的,是冠军的小点,他们会失去冠军。用
表示与点
相邻的小点,权值是
并且是冠军的集合。所以当一个小点
是冠军的时候,不妨枚举相邻的大点
,把
塞进
。当更改的时候就可以暴力遍历
里面的点并修改状态
注意到总 个数是
级别,所以是没有问题的。时间复杂度
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, m, q, S;
vector<int> e[N], big[N];
int walk[N], mx[N];
int ans[N], chmp[N];
vector<pair<int, int>> had[10005];
int main() {
cin >> n >> m >> q;
S = 2 * sqrt(m) + 1;
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);
}
for (int i = 1; i <= n; i++)
for (int v : e[i])
if (e[v].size() >= S)
big[i].push_back(v);
for (int i = 1; i <= q; i++) {
int u, w;
scanf("%d%d", &u, &w);
walk[u] += w;
had[walk[u]].push_back({ u, i });
}
vector<int> f(n + 1, q), last(n + 1, q), mn(n, q);
for (int w = 10000; w >= 1; w--) {
for (int i = 0; i < had[w].size(); i++) {
int u = had[w][i].first, t = had[w][i].second;
for (auto v : big[u]) mn[v] = min(mn[v], t);
last[u] = f[u], f[u] = t;
}
for (int i = 0; i < had[w].size(); i++) {
int u = had[w][i].first, t = had[w][i].second;
if (e[u].size() < S) {
int r = last[u];
for (auto v : e[u]) r = min(r, f[v]);
ans[u] += max(0, r - t);
} else
ans[u] += max(0, min(last[u], mn[u]) - t);
}
}
for (int i = 1; i <= n; i++) printf("%d\n", ans[i]);
return 0;
}