校内模拟赛赛后总结
前记(建议看一下)
(开始写于 2023.9.92023.9.9)
(以前的比赛我就记个排名得了,懒得补了)
(本来是不想写的,但是看到学长以及身边人都写,思考了一下,感觉有点用,于是也写了)
(里面也写了一些闲话)
至于为什么把所有场的写一起了,因为后期想找题方便,万一哪天想起来哪场模拟赛里的某道题,直接在这 Ctrl+FCtrl+F,不用一个一个翻了。
所以我还写了一个分着的,给出目录,想看某一场的直接从目录穿过去就好了。
7.20 ~ 7.22 CSP 模拟 1 ~ 3 没考
7.24 CSP 模拟 4 (rk 6)
7.25 CSP 模拟 5 (rk 3)
7.26 CSP 模拟 6 (rk 23)
7.27 CSP 模拟 7 (rk 12)
7.28 CSP 模拟 8 (rk 9)
7.30 CSP 模拟 9 (rk 6)
7.31 CSP 模拟 10 (rk 23)
8.1 CSP 模拟 11 (rk 8)
8.2 CSP 模拟 12 (rk 11)
8.3 CSP 模拟 13 (rk 18)
8.4 CSP 模拟 14 (rk 23)
8.7 CSP 模拟 15 (rk 15)
8.9 CSP 模拟 16 (rk 20)
8.10 CSP 模拟 17 (rk 18)
8.11 CSP 模拟 18 (rk 9)
8.12 CSP 模拟 19 (rk 22)
8.13 CSP 模拟 20 (rk 6)
8.14 CSP 模拟 21 (rk 21)
8.16 CSP 模拟 22 (rk 4)
8.17 CSP 模拟 23 (rk 8)
8.18 CSP 模拟 24 (rk 10)
8.19 CSP 模拟 25 (rk 12)
8.20 CSP 模拟 26 (rk 30)
8.21 CSP 模拟 27 (rk 6)
8.23 CSP 模拟 28 (rk 25)
8.24 CSP 模拟 29 (rk 29)
8.25 CSP 模拟 30 (rk 26)
8.25+ CSP 模拟 30++ (rk 29)
8.26 CSP 模拟 31 (rk 24)
暑假模拟赛总结(补)
当时一直没有写每场比赛的赛后总结,现在没时间补那么多了,就写个总的吧。
一共打了 2929 场,22 次前 55,1111 次前 1010,77 次 11∼2011∼20,1111 次 2121 及以后。
26∼3126∼31 场的时候,感觉状态不对,基本场场垫底,至于为什么,我也不知道,我真的不知道,感觉没发生什么事啊,我也在一如既往地学着。
感觉过了一个暑假,更让我看清楚和别人的差距了。感觉我刚摸着门道,别人却早就知道怎么学奥赛了。嗯,反正崩是不会崩的,好好学吧。
9.1 CSP 模拟 32 (rk 25)
9.9 CSP 模拟 33 (rk 14)
T1 光
(乱搞的)
非正解做法,但是跑的飞快,碾压正解。
首先你得会暴力枚举,O(n3)O(n3) 或 O(n4)O(n4) 都行,我用的 O(n3)O(n3)。
先将原来的四个数同除一个数 KK,跑一边,再将跑出来的四个答案 −4K−4K 作为下边界,+4K+4K 作为上边界。原来的四个数的答案就在这个区间浮动,枚举区间就变成了 8K8K,即时间复杂度为 O((8K)3)O((8K)3)。我的 KK 取的是 max(a,b,c,d)4max(a,b,c,d)4,也有人取的一个常数。
代码
点击查看代码
#include <bits/stdc++.h>
using namespace std;
struct Sol {
int t1, t2, t3, t4;
};
inline pair<int, Sol> judge(int a, int lim_a_min, int lim_a_max, int b, int lim_b_min, int lim_b_max, int c, int lim_c_min, int lim_c_max, int d) {
int ans = INT_MAX, t[5];
Sol s;
for(int i = max(lim_a_min, 0); i <= lim_a_max; ++ i) {
t[1] = a;
t[2] = b;
t[3] = c;
t[4] = d;
t[1] = t[1] - i;
t[2] = t[2] - i / 2;
t[3] = t[3] - i / 2;
t[4] = t[4] - i / 4;
if(t[1] < 0 && t[2] < 0 && t[3] < 0 && t[4] < 0) {
if(i < ans) {
ans = i;
s = {i, 0, 0, 0};
}
break;
}
for(int j = max(lim_b_min, 0); j <= lim_b_max; ++ j) {
t[1] = t[1] - j / 2;
t[2] = t[2] - j;
t[3] = t[3] - j / 4;
t[4] = t[4] - j / 2;
if(t[1] < 0 && t[2] < 0 && t[3] < 0 && t[4] < 0) {
t[1] = t[1] + j / 2;
t[2] = t[2] + j;
t[3] = t[3] + j / 4;
t[4] = t[4] + j / 2;
if(i + j < ans) {
ans = i + j;
s = {i, j, 0, 0};
}
break;
}
for(int k = max(lim_c_min, 0); k <= lim_c_max; ++ k) {
t[1] = t[1] - k / 2;
t[2] = t[2] - k / 4;
t[3] = t[3] - k;
t[4] = t[4] - k / 2;
if(t[1] < 0 && t[2] < 0 && t[3] < 0 && t[4] < 0) {
t[1] = t[1] + k / 2;
t[2] = t[2] + k / 4;
t[3] = t[3] + k;
t[4] = t[4] + k / 2;
if(i + j + k < ans) {
ans = i + j + k;
s = {i, j, k, 0};
}
break;
}
int maxn = max({t[4], t[3] * 2, t[2] * 2, t[1] * 4});
if(i + j + k + maxn < ans) {
ans = i + j + k + maxn;
s = {i, j, k, maxn};
}
t[1] = t[1] + k / 2;
t[2] = t[2] + k / 4;
t[3] = t[3] + k;
t[4] = t[4] + k / 2;
}
t[1] = t[1] + j / 2;
t[2] = t[2] + j;
t[3] = t[3] + j / 4;
t[4] = t[4] + j / 2;
}
}
return {ans, s};
}
signed main() {
int a, b, c, d;
cin >> a >> b >> c >> d;
int maxn = max({a, b, c, d}), K = sqrt(maxn / 4), lim = maxn / K;
pair<int, Sol> ji = judge(a / K, 0, lim, b / K, 0, lim, c / K, 0, lim, d / K);
cout << judge(a, ji.second.t1 * K - K * 4, ji.second.t1 * K + K * 4, b, ji.second.t2 * K - K * 4, ji.second.t2 * K + K * 4, c, ji.second.t3 * K - K * 4, ji.second.t3 * K + K * 4, d).first <<'\n';
}
/*
50 24 25 12
50
*/
/*
8 8 8 8
15
*/
/*
49 47 42 11
76
*/
/*
50 49 26 31
71
*/
/*
1500 1500 1500 1500
2668
*/
/*
400 400 400 400
712
*/
T2 爬
其实挺可做的。
异或,所以按位考虑。
先维护一个点的 sonson,即他的儿子数 +1+1(他自己)
对于每一个点的每一位,记录他和他儿子里有多少个点,这一位为 11,拿 cntcnt 记一下。
对于第 ii 个点的第 jj 位,如果 3≤cnt[i][j]3≤cnt[i][j] 这一位产生的贡献为
否则为
根节点特判一下,不写了。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 100005
#define mod 1000000007
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, a[M], fa[M], bin[M], cnt[M][32], ans, son[M];
signed main() {
n = read();
bin[0] = 1;
for(int i = 1; i <= n; ++ i)
bin[i] = (bin[i - 1] << 1) % mod;
for(int i = 1; i <= n; ++ i)
a[i] = read();
for(int i = 2; i <= n; ++ i) {
fa[i] = read();
++ son[fa[i]];
++ son[i];
for(int j = 0; j <= 29; ++ j)
if(bin[j] & a[i])
++ cnt[i][j], ++ cnt[fa[i]][j];
}
for(int j = 0; j <= 29; ++ j) {
if(bin[j] & a[1]) {
if(cnt[1][j] >= 2)
ans = (ans + ((bin[cnt[1][j] - 1] - 1) * bin[n - cnt[1][j] - 1] % mod + bin[n - son[1] - 1] * (bin[son[1] - cnt[1][j]] - 1) % mod) % mod * bin[j] % mod) % mod;
else
ans = (ans + (bin[son[1] - cnt[1][j]] - 1) % mod * bin[n - son[1] - 1] % mod * bin[j] % mod) % mod;
}
else
if(cnt[1][j] >= 1)
ans = (ans + bin[cnt[1][j] - 1] * bin[n - cnt[1][j] - 1] % mod * bin[j] % mod) % mod;
}
for(int i = 2; i <= n; ++ i)
for(int j = 0; j <= 29; ++ j)
if(cnt[i][j] >= 3)
ans = (ans + ((bin[cnt[i][j] - 1] - cnt[i][j] + mod) % mod * bin[n - cnt[i][j] - 1] % mod + cnt[i][j] * (bin[son[i] - cnt[i][j]] - 1 + mod) % mod * bin[n - son[i] - 1] % mod) % mod * bin[j] % mod) % mod;
else
if(cnt[i][j] >= 1)
ans = (ans + cnt[i][j] * (bin[son[i] - cnt[i][j]] - 1) % mod * bin[n - son[i] - 1] % mod * bin[j] % mod) % mod;
write(ans);
}
/*
5
5 3 1 9 3
1 1 2 2
126
*/
T3 字符串
首先对三条规则进行简单分析
- 对于规则 3 来说,假设 c=3c=3,那么如果想要通过 AB 切换来获得价值,那字符串就应该形如 BBBABBBABBBA 这样,即每 3 个 B 就切换成 A。
- 对于规则 1.2 来说,把字母放在连续的一段地方,第一次产生价值需要 a+1a+1 个 A 或者 b+1b+1 个 B,第二次及以后产生价值只需要 aa 个 A 或者 bb 个 B
我们可以枚举 A 和 B 切换了多少次,假定我们切换的方式就是 BBBABBBABBBA,即每一段 A 的长度都为 1,我们又知道 AB 的切换次数,就可以知道现在已经用掉了几个 A,然后我们先考虑把 A 全部用完。
- 在 BBBABBBA 的形式前面 只需要花费一个 A 就可以拿到一个价值
- 然后将多余的 A 填充进字符串中,因为每一段 A 的长度都是 1,所以此时本质上是每多 aa 个 A,答案 +1。
然后再考虑将剩余的 B 用完。
- 如果倒数第二个位置是 A 的话,那么最后一个只需要花费一个 B 就能拿到一个价值
- 例如 b=4,c=3b=4,c=3,本来我们填充的是 BBBABBBABBBA,根据规则 2,当一段 B 的长度达到 5 的时候又可以使得价值 +1,所以我们尽量将每一段 B 都填充到长度为 5。如果全部填充好了且还有多余的 B,那么每多 bb 个 B,答案 +1。
代码
点击查看代码
#include <bits/stdc++.h>
#define M 100005
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, m, a, b, c, T, ans, sum;
signed main() {
T = read();
for(int t = 1; t <= T ; ++ t) {
n = read();//n 个 A
m = read();//m 个 B
a = read();//a 个 A +1
b = read();//b 个 B +1
c = read();//至少 c 个 B 换 A
ans = sum = 2 + (n - 1) / a + (m - 1) / b;
for(int i = 1; i <= min(n, m / c); ++ i) {
sum = 2 + (i - 1) * 2;
if(n - i >= 1)
sum += (n - i - 1) / a + 1;
if(c > b)
sum += (c - 1) / b * i;
if(m - c * i >= 1) {
++ sum;
int remain = m - c * i - 1, need = (((c - 1) / b + 1) * b) - (c - 1);
if(remain / need >= i)
sum += i, remain -= need * i;
else
sum += remain / need, remain -= remain / need * need;
sum += remain / b;
}
ans = max(ans, sum);
}
write(ans);
putchar('\n');
}
}
/*
6
1 1 1 1 1
5 4 3 3 2
5 5 3 3 2
3 9 3 3 3
7 3 3 5 8
4 7 2 8 5
2
5
6
8
4
5
*/
T4 奇怪的函数
嗯,挺好的一道题,可能是因为我以前没见过这类的吧。
看到又有操作又有查询,想到线段树。
线段树维护操作。
正常的线段树的合并,一般写为 f(f(ls),f(rs))f(f(ls),f(rs)),f(x)f(x) 可以代表取最大值最小值,相加,相乘等等等等。
但是也可以写作 g(f(ls))g(f(ls)),这里的 ff 和 gg 与我们维护的东西有关。
比如这道题。我们发现总的操作总能写成 max(min(x+v,l),r)max(min(x+v,l),r) 的形式。
怎么合并?
我们知道:
那么设左儿子 f(x)=max(min(x+v,l),r)f(x)=max(min(x+v,l),r),右儿子 g(x)=max(min(x+V,L),R)g(x)=max(min(x+V,L),R)
我们先设 c=min(max(l+V,r+V),L)c=min(max(l+V,r+V),L),免得我一会写的太长。
那么对于这个节点的 h(x)h(x),有:
设 h(x)=max(min(x+a,c),b)h(x)=max(min(x+a,c),b)
我们发现 cc 就等于我们上面设的那个 cc,a=v+Va=v+V,b=max(min(r+V,c),R)b=max(min(r+V,c),R)
于是就会 push_uppush_up 了。
这道题就没有难点了。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 100005
#define mod 1000000007
#define pair<int, int> P
#define make_pair MP
#define time() chrono::system_clock::now().time_since_epoch().count()
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, q, op[M], val[M], minn[M << 2], maxn[M << 2], v[M << 2];
void build(int rt, int l, int r) {
if(l == r) {
if(op[l] == 1) {
minn[rt] = INT_MAX;
maxn[rt] = 0;
v[rt] = val[l];
}
if(op[l] == 2) {
minn[rt] = val[l];
maxn[rt] = 0;
v[rt] = 0;
}
if(op[l] == 3) {
minn[rt] = INT_MAX;
maxn[rt] = val[l];
v[rt] = 0;
}
return;
}
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
build(ls, l, mid);
build(rs, mid + 1, r);
int c = min(max(minn[ls] + v[rs], maxn[ls] + v[rs]), minn[rs]);
maxn[rt] = max(min(maxn[ls] + v[rs], c), maxn[rs]);
minn[rt] = c;
v[rt] = v[ls] + v[rs];
}
void change(int rt, int l, int r, int pos) {
if(l == r) {
if(op[l] == 1) {
minn[rt] = INT_MAX;
maxn[rt] = 0;
v[rt] = val[l];
}
if(op[l] == 2) {
minn[rt] = val[l];
maxn[rt] = 0;
v[rt] = 0;
}
if(op[l] == 3) {
minn[rt] = INT_MAX;
maxn[rt] = val[l];
v[rt] = 0;
}
return;
}
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
if(pos <= mid)
change(ls, l, mid, pos);
else
change(rs, mid + 1, r, pos);
int c = min(max(minn[ls] + v[rs], maxn[ls] + v[rs]), minn[rs]);
maxn[rt] = max(min(maxn[ls] + v[rs], c), maxn[rs]);
minn[rt] = c;
v[rt] = v[ls] + v[rs];
}
signed main() {
n = read();
for(int i = 1; i <= n; ++ i) {
op[i] = read();
val[i] = read();
}
build(1, 1, n);
q = read();
for(int i = 1; i <= q; ++ i) {
int opp = read();
if(opp == 4) {
int x = read();
write(max(min(x + v[1], minn[1]), maxn[1]));
putchar('\n');
}
else {
int pos = read(), vall = read();
op[pos] = opp;
val[pos] = vall;
change(1, 1, n, pos);
}
}
}
/*
10
1 48
1 50
1 180
2 957
1 103
1 100
1 123
3 500
1 66
1 70
3
4 20
4 50
4 700
760
790
1419
*/
9.10 CSP 模拟 34 (rk 17)
T1 斐波那契树
考场想了个差不多,打完正解以后准备打对拍,打完对拍和暴力电脑死机了,然后就重启了,然后就拍子和暴力没了,然后就不想再打一遍了,然后就正解打炸了,,,
挺正常的一个 T1T1,颜色是白色的边权设成 11,黑色的设成 00,跑一边最小生成树和最大生成树,求出总权值。可证,在最小权值与最大权值之间的所有值,都可以被构建出相应的树。于是就找这两个值之间是否存在一个斐波那契数。
当然,还得判断一下是否连通。
代码
点击查看代码
#include <bits/stdc++.h>
#define M 200005
#define P pair<int, int>
#define MP make_pair
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, T, m, fa[M], fib[M], tot, res;
bool vis[M];
pair<int, P> pa[M];
int find(int x) {
while(x != fa[x])
x = fa[x] = fa[fa[x]];
return x;
}
inline bool cmp1(pair<int, P> a, pair<int, P> b) {
return a.first < b.first;
}
inline bool cmp2(pair<int, P> a, pair<int, P> b) {
return a.first > b.first;
}
inline bool Kruskal(int op) {
for(int i = 1; i <= n; ++ i)
fa[i] = i;
res = 0;
tot = 0;
if(op == 1)
stable_sort(pa + 1, pa + 1 + 2 * m, cmp1);
else
stable_sort(pa + 1, pa + 1 + 2 * m, cmp2);
for(int i = 1; i <= 2 * m; ++ i) {
int x = find(pa[i].second.first), y = find(pa[i].second.second);
if(x == y)
continue;
res += pa[i].first;
fa[x] = fa[y];
if(++ tot == n - 1)
break;
}
return tot == n - 1;
}
signed main() {
T = read();
fib[0] = 1;
fib[1] = 1;
for(int i = 2; i <= 30; ++ i)
fib[i] = fib[i - 1] + fib[i - 2];
for(int t = 1; t <= T; ++ t) {
n = read();
m = read();
for(int i = 1; i <= n; ++ i)
fa[i] = i;
for(int i = 1; i <= m; ++ i) {
int u = read(), v = read(), color = read();
pa[i * 2 - 1] = MP(color, MP(u, v));
pa[i * 2] = MP(color, MP(v, u));
}
if(!Kruskal(1)) {
printf("NO\n");
continue;
}
int lim = res;
Kruskal(2);
int ji = 1, flag = 0;
while(fib[ji] <= res) {
if(fib[ji] >= lim)
flag = 1;
++ ji;
}
if(flag == 1)
printf("YES\n");
else
printf("NO\n");
}
}
/*
2
4 4
1 2 1
2 3 1
3 4 1
1 4 0
5 6
1 2 1
1 3 1
1 4 1
1 5 1
3 5 1
4 2 1
YES
NO
*/
T2 期末考试
赛时因为一些玄学问题,挂了 3030,赛后用异或哈希 ++ 状压,两个全炸了,一个负责 WAWA,一个负责 TT,,,
我发誓这是我最后一次用异或哈希,,,
题还是挺好的,第一次了解到 meetinthemiddlemeetinthemiddle 这个东西。
先枚举前五道题的答案,用哈希存一下每个状态有多少种情况,再去枚举后五个。打的丑的话会卡常,,,
代码
点击查看代码
#include<bits/stdc++.h>
#define M 200005
#define int long long
#define mod 993244853
#define time() chrono::system_clock::now().time_since_epoch().count()
using namespace std;
int T, n, bin[31], Pow[11], hash_qian, hash_hou, R, ans, sc, grade[M], s[M];
string str;
unordered_map<int, int> mp;
void Hash() {
Pow[0] = R;
for(int i = 1; i <= 10; ++ i) {
Pow[i] = Pow[i - 1] << 13;
Pow[i] ^= Pow[i] >> 17;
Pow[i] ^= Pow[i] << 5;
Pow[i] %= mod;
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
srand(time());
R = 131;
Hash();
cin >> T;
for(int t = 1; t <= T; ++ t) {
mp.clear();
cin >> n;
ans = 0;
for(int i = 1; i <= n; ++ i) {
cin >> str >> grade[i];
grade[i] /= 10;
s[i] = 0;
for(int j = 9; j >= 0; -- j)
s[i] = (s[i] << 2) + (str[j] - 'A');
}
for(int i = 0; i <= 1023; ++ i) {
bool flag = 0;
hash_qian = 0;
for(int j = 1; j <= n; ++ j) {
int x = 3;
sc = 0;
for(int k = 1; k <= 5; ++ k) {
if((x & i) == (s[j] & x))
++ sc;
x <<= 2;
}
if(sc > grade[j]) {
flag = 1;
break;
}
hash_qian = (hash_qian * R + Pow[sc]) % mod;
// hash_qian = (hash_qian + Pow[sc]) << 13;
// hash_qian ^= hash_qian >> 17;
// hash_qian ^= hash_qian << 5;
// hash_qian %= mod;
}
if(!flag)
++ mp[hash_qian];//, cout << hash_qian << '\n';
}
for(int i = 1; i <= n; ++ i)
s[i] >>= 10;
for(int i = 0; i <= 1023; ++ i) {
bool flag = 0;
hash_hou = 0;
for(int j = 1; j <= n; ++ j) {
int x = 3;
sc = grade[j];
for(int k = 1; k <= 5; ++ k) {
if((x & i) == (s[j] & x))
-- sc;
x <<= 2;
}
// cout << "!!" << sc << '\n';
if(sc < 0 || sc > 5) {
flag = 1;
break;
}
hash_hou = (hash_hou * R + Pow[sc]) % mod;
// hash_hou = (hash_hou + Pow[sc]) << 13;
// hash_hou ^= hash_hou >> 17;
// hash_hou ^= hash_hou << 5;
// hash_hou %= mod;
}
if(!flag)
ans += mp[hash_hou];//, cout << "!!!" << hash_hou << '\n';
}
cout << ans << '\n';
}
}
/*
3
3
AAAAAAAAAA 0
BBBBBBBBBB 0
CCCCCCCCCC 0
1
CCCCCCCCCC 90
2
AAAAAAAAAA 10
ABCDABCDAB 20
1
30
57456
*/
T3 麻烦的工作
考虑一种贪心做法,把任务按所需人数从大到小排序,优先安排需要人数多的任务,尽可能把任务安排给剩余可做任务数多的人,这样显然最优。
考虑这个贪心做法的本质,假设我们考虑到第 kk 个任务,则每个员工至多被安排 min(bi,k)min(bi,k) 个任务,总共有 m∑i=1min(bi,k)m∑i=1min(bi,k),我们需要保证对于任意 kk,m∑i=1min(bi,k)≥k∑i=1a[i]m∑i=1min(bi,k)≥k∑i=1a[i]。
考虑怎么维护这个限制。
首先考虑不修改 aa 数组的情况。我们考虑改为统计可做任务数 ≥i≥i 的人有多少个,称这个数组为 valval(可以理解为坐标轴上 xx 轴是员工,yy 轴是该员工的任务数,然后以 yy 轴为变量重写这个表达式)。这样条件变成了对所有的 kk,k∑i=1val[i]−a[i]≥0k∑i=1val[i]−a[i]≥0,这是一个前缀和,所以可以O(1)计算。修改时,可以使用一个以上式 ii 为下标,维护上式最小值的线段树,只要所有的数都大于等于 00,则有合法方案。修改操作就是单点修改。
再考虑怎么对 aa 数组进行修改,我们需要保证 aa 数组是排好序的,这样才能保证维护的线段树不会出错.
观察到每次只会有 11 的变化量,那么我们考虑如何避免重新排序,假设当前修改的 a[i]=xa[i]=x,我们可以把它等价成修改任意一个其他的满足 a[j]=xa[j]=x 的位置,则对于加操作,我们修改排序中最靠前的一个,对于减操作,我们修改排序中最靠后的一个,这样就不需要重新排序了。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 1000005
#define mod 1000000007
#define P pair<int, int>
#define MP make_pair
#define time() chrono::system_clock::now().time_since_epoch().count()
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, m, q, T, a[M], b[M], tree[M << 2], val[M], qian[M], lazy[M << 2], c[M];
inline bool cmp(int a, int b) {
return a > b;
}
inline void push_down(int rt) {
if(lazy[rt]) {
int ls = rt << 1, rs = ls | 1;
tree[ls] += lazy[rt];
tree[rs] += lazy[rt];
lazy[ls] += lazy[rt];
lazy[rs] += lazy[rt];
lazy[rt] = 0;
}
}
void build(int rt, int l, int r) {
if(l == r) {
tree[rt] = qian[l];
return;
}
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
build(ls, l, mid);
build(rs, mid + 1, r);
tree[rt] = min(tree[ls], tree[rs]);
}
void change(int rt, int l, int r, int la, int ra, int addend) {
if(la <= l && r <= ra) {
tree[rt] += addend;
lazy[rt] += addend;
return;
}
push_down(rt);
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
if(la <= mid)
change(ls, l, mid, la, ra, addend);
if(ra > mid)
change(rs, mid + 1, r, la, ra, addend);
tree[rt] = min(tree[ls], tree[rs]);
}
signed main() {
T = 1;
for(int t = 1; t <= T; ++ t) {
n = read();
m = read();
for(int i = 1; i <= n; ++ i)
a[i] = read(), c[i] = a[i];
stable_sort(a + 1, a + 1 + n, cmp);
for(int i = 1; i <= m; ++ i)
b[i] = read(), ++ val[b[i]];
for(int i = n; i >= 0; -- i)
val[i] += val[i + 1];
for(int i = 1; i <= n; ++ i)
qian[i] = val[i] - a[i] + qian[i - 1];//, cout << i << " " << val[i] << " " << a[i] << ' ' << qian[i] << endl;
build(1, 1, n);
q = read();
for(int i = 1; i <= q; ++ i) {
int op = read(), x = read();
if(op == 1) {
int pos = lower_bound(a + 1, a + 1 + n, c[x], greater<int>()) - a;
++ c[x];
++ a[pos];
change(1, 1, n, pos, n, -1);
}
if(op == 2) {
int pos = upper_bound(a + 1, a + 1 + n, c[x], greater<int>()) - a - 1;
-- c[x];
-- a[pos];
change(1, 1, n, pos, n, 1);
}
if(op == 3) {
++ b[x];
change(1, 1, n, b[x], n, 1);
}
if(op == 4) {
change(1, 1, n, b[x], n, -1);
-- b[x];
}
if(tree[1] >= 0)
putchar('1');
else
putchar('0');
putchar('\n');
}
}
}
/*
5 5
1 5 2 4 3
3 3 3 3 3
5
4 2
3 5
2 2
1 1
1 4
0
1
1
1
0
*/
T4 小X的 的Galgame
(至于为什么题目里两个"的"中间还有一个空格,我也不知道)
我就今天没逆序开题,就今天 T4T4 好写,,,
树形dp吧算是。
treeu,1treeu,1 代表 uu 的子树里有存档点的最小花费,treeu,0treeu,0 代表 uu 的子树里没存档点的最小花费。
考虑怎么去转移。
维护一个 sizeusizeu 代表 uu 的子树里有多少个叶结点,sonusonu 代表 uu 的儿子,disudisu 代表从根结点 11 到结点 uu 的距离。
那么有:
但是,我们发现 treeu,1treeu,1 不像我们想的那样简单,如果我们任意一次决策时选择了 minmin 函数里的后者,我们可以少走一次 disudisu 的距离。因为既然 uu 的子树里已经放了存档点了,那么 uu 结点也放一个存档点一定更优。
于是我们还得考虑一下,对于全部选前者的情况,是否选一次后者更优,于是再维护一个 minnminn,代表对于所有选择前者的情况 treev,1+disv−treev,0−wu,v∗size[v]treev,1+disv−treev,0−wu,v∗size[v] 的最小值。如果最后我们的决策告诉我们全选前者较优,那我们再判断 00 和 minn−disuminn−disu 的大小,看看是否选一次后者更优。
最后 min(tree1,1,tree1,0)min(tree1,1,tree1,0) 就是答案。
代码
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define M 100005
using namespace std;
int n, dis[M], tree[M][2], size[M];
vector<pair<int, int> > edge[M];
void dfs(int u, int fu, int t) {
dis[u] = dis[fu] + t;
int minn = LONG_LONG_MAX, cnt_1 = 0, cnt_0 = 0;
for(int i = 0; i < edge[u].size(); ++ i) {
int v = edge[u][i].first, tt = edge[u][i].second;
if(v == fu)
continue;
dfs(v, u, tt);
tree[u][0] += tree[v][0] + tt * size[v];
if(tree[v][1] + dis[v] < tree[v][0] + tt * size[v]) {
tree[u][1] += tree[v][1] + dis[v];
++ cnt_1;
}
else {
tree[u][1] += tree[v][0] + tt * size[v];
minn = min(minn, tree[v][1] + dis[v] - tree[v][0] - tt * size[v]);
++ cnt_0;
}
size[u] += size[v];
}
if(cnt_1 + cnt_0 == 0) {
size[u] = 1;
return;
}
if(cnt_1)
tree[u][1] -= dis[u];
else
tree[u][1] += min(0ll, minn - dis[u]);
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i < n; ++ i) {
int u, v, t;
cin >> u >> v >> t;
edge[u].push_back({v, t});
edge[v].push_back({u, t});
}
dfs(1, 0, 0);
cout << min(tree[1][1], tree[1][0]) << '\n';
}
/*
8
1 2 1
3 1 1
2 4 2
5 4 2
6 2 2
3 7 3
8 3 3
14
*/
/*
10
1 2 707939188
2 3 782074741
3 4 46843572
4 5 615425450
1 6 218457033
5 7 644550627
3 8 117264669
7 9 802389431
5 10 634181190
4569125901
*/
9.11 CSP 模拟 35 (rk 3)
T1 一般图最小匹配
暴力最高分,所以先讲暴力。
考场想了个错解(贪心),然后又细想了想怎么让错解对的更多。
维护整个数列的从小到大的排序时间复杂度不太行,于是就想到值维护最小值。
但是值维护最小值不行啊,太容易 WAWA 了,于是维护了次小值。
然后,,,
次次小值,次次次小值,,,
然后再测试点分治,n≤1000n≤1000 的维护排序后的序列,其他的维护最小值,次小值,次次小值,次次次小值,,,
然后就是暴力最高分,7575
考场暴力代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 5005
#define mod 1000000007
#define P pair<int, int>
#define MP make_pair
#define time() chrono::system_clock::now().time_since_epoch().count()
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, T, m, a[M], ans, tong[M];
signed main() {
T = 1;
for(int t = 1; t <= T; ++ t) {
n = read();
m = read();
for(int i = 1; i <= n; ++ i)
a[i] = read();
if(n > 1000) {
priority_queue<pair<int, P>, vector<pair<int, P>>, greater<pair<int, P>> > zong;
for(int i = 1; i <= n ; ++ i) {
int minn = LONG_LONG_MAX, ci_minn = LONG_LONG_MAX, cci_minn = LONG_LONG_MAX, ccci_minn = LONG_LONG_MAX, ii = 0, jj = 0, ci = 0, cj = 0, cci = 0, ccj = 0, ccci, cccj;
for(int j = 1; j <= n; ++ j) {
if(i == j)
continue;
int ji = abs(a[i] - a[j]);
if(ji <= minn)
ccci_minn = cci_minn, cci_minn = ci_minn, ci_minn = minn, minn = ji, ccci = cci, cccj = ccj, cci = ci, ccj = cj, ci = ii, cj = jj, ii = i, jj = j;
else
if(ji < ci_minn)
ccci_minn = cci_minn, cci_minn = ci_minn, ci_minn = ji, ccci = cci, cccj = ccj, cci = ci, ccj = cj, ci = i, cj = j;
else
if(ji < cci_minn)
ccci_minn = cci_minn, cci_minn = ji, ccci = cci, cccj = ccj, cci = i, ccj = j;
else
if(ji < ccci_minn)
ccci_minn = ji, ccci = i, cccj = j;
}
zong.push(MP(minn, MP(ii, jj)));
zong.push(MP(ci_minn, MP(ci, cj)));
zong.push(MP(cci_minn, MP(cci, ccj)));
zong.push(MP(ccci_minn, MP(ccci, cccj)));
}
for(int i = 1; i <= m && (!zong.empty()); ++ i) {
while(tong[zong.top().second.first] || tong[zong.top().second.second])
zong.pop();
ans += zong.top().first;
tong[zong.top().second.first] = 1;
tong[zong.top().second.second] = 1;
}
}
else {
priority_queue<pair<int, P>, vector<pair<int, P>>, greater<pair<int, P>> > q[M], zong;
for(int i = 1; i <= n ; ++ i) {
for(int j = 1; j < i; ++ j) {
zong.push(MP(abs(a[i] - a[j]), MP(i, j)));
}
}
for(int i = 1; i <= m; ++ i) {
while(tong[zong.top().second.first] || tong[zong.top().second.second])
zong.pop();
ans += zong.top().first;
tong[zong.top().second.first] = 1;
tong[zong.top().second.second] = 1;
}
}
write(ans);
}
}
/*
4 1
2 4 7 3
1
*/
/*
8 3
9 2 3 12 11 7 6 5
3
*/
卡的话也比较容易,让 1001≤n1001≤n,并让 nmnm 尽可能小,就 WAWA 了。
然后来讲正解。
一个显然的事实是 一点都不显然 :
最优匹配只会考虑匹配排序后相邻的两个元素。
考虑先排序,fi,jfi,j 表示前 ii 个元素中,选取 jj 对匹配的最小值。
不选当前元素时 fi,j=fi−1,jfi,j=fi−1,j
选取当前元素时 fi,j=fi−2,j+ai−ai−1fi,j=fi−2,j+ai−ai−1
两者取 minmin 即可。总复杂度 O(nm)O(nm)
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 5005
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, T, m, a[M], dp[M][M], ans;
signed main() {
T = 1;
for(int t = 1; t <= T; ++ t) {
n = read();
m = read();
for(int i = 1; i <= n; ++ i)
a[i] = read();
stable_sort(a + 1, a + 1 + n);
ans = LONG_LONG_MAX;
memset(dp, 0x3f, sizeof(dp));
dp[0][0] = dp[1][0] = 0;
for(int i = 2; i <= n; ++ i) {
dp[i][0] = 0;
for(int j = 1; j <= min(i / 2, m); ++ j)
dp[i][j] = min(dp[i - 2][j - 1] + a[i] - a[i - 1], dp[i - 1][j]);
ans = min(ans, dp[i][m]);
}
write(ans);
}
}
/*
4 1
2 4 7 3
1
*/
/*
8 3
9 2 3 12 11 7 6 5
3
*/
/*
10 2
606771589 56825125 185482190 132818542 406283233 460383919 497973450 244096167 17502318 238309819
*/
如果压一维会跑得很快,至少比我这个快 10∼2010∼20 倍。
T2 重定向
(场切,但是写的奇丑无比)
这里讲个人思路,但毕竟是考场思路,有些地方比较麻烦。所以建议去看官方题解。
先看的数据范围,嗯,O(n)O(n)。
考虑直接去扫一遍,但是不知道扫到什么地方去决策。
从左往右扫,可以从最左边先弄出一段来,保证这一段里不需要操作,即已经最优了。(这一段的长度可以为 00)
怎么找这一段呢?
设 a[i]a[i] 为 ii 位置上的数,tong[i]tong[i] 表示 ii 出现的位置,没出现过就是 00。
从左往右枚举 pos1pos1,如果满足
这两个条件中的任意一个,就继续枚举。
如果 a[pos1]=0a[pos1]=0 且 tong[pos1]≠0tong[pos1]≠0
那就好说了,直接把 tong[pos1]tong[pos1] 那个位置删掉,拿来用。
然后直接走到尾就行了,但是 tong[pos1]tong[pos1] 那个位置不输出。
但是如果 a[pos1]≠pos1a[pos1]≠pos1 且 a[pos1]≠0a[pos1]≠0
那还是挺麻烦的,这一段已经到头了,需要进行其他处理。
如果 a[pos1]<a[pos1+1]a[pos1]<a[pos1+1],继续枚举 pos1pos1。
如果 a[pos1]>a[pos1+1]a[pos1]>a[pos1+1] 继续分情况讨论:
如果 a[pos1+1]=0a[pos1+1]=0
如果存在 pos3pos3 小于 pos1pos1 且没有被用到过且 tong[pos3]=0tong[pos3]=0,那么就删除当前位置,让下一个位置为 pos3pos3
否则,设 pos2pos2 为以前没有被用到过的,且 tong[pos2]=1tong[pos2]=1 的最小的数,那么删掉位置 tong[pos2]tong[pos2]
如果 a[pos1]<a[pos1+1]a[pos1]<a[pos1+1],那么直接删掉 pos1pos1。
嗯,如果这次枚举已经删掉了一个位置,那么以后就不要再删了。
基本讲完了,但是发现还会有 1 2 3 4…n1 2 3 4…n 这种情况以及 0 0 0…0 0 0… 这种情况,这些情况会导致我们输出 nn 个数,所以我们不能一边扫一遍输出,得把要输出的答案记下来,存到 ansans 数组里,最后只输出 ansans 数组的 1∼n−11∼n−1 位这些数就行了。
代码
点击查看代码
#include <bits/stdc++.h>
#define M 200005
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, T, m, q, a[M], tong[M], ban, pos1, pos2, ans[M], cnt, pos3;
signed main() {
T = read();
for(int t = 1; t <= T; ++ t) {
for(int i = 1; i <= n; ++ i)
tong[a[i]] = 0;
n = read();
for(int i = 1; i <= n; ++ i)
a[i] = read(), tong[a[i]] = i;
pos1 = 1, pos2 = 1, ban = 0, cnt = 0, pos3 = 1;
if(a[1] <= 1) {
for(; pos1 <= n; ++ pos1) {
if(a[pos1] == pos2) {
ans[++ cnt] = pos2;
++ pos2, ++ pos3;
continue;
}
if(a[pos1] == 0) {
if(tong[pos2]) {
ans[++ cnt] = pos2;
ban = tong[pos2];
++ pos2;
++ pos3;
++ pos1;
break;
}
ans[++ cnt] = pos2;
++ pos2, ++ pos3;
continue;
}
break;
}
if(ban) {
for(; pos1 <= n; ++ pos1) {
if(pos1 == ban)
continue;
if(a[pos1])
ans[++ cnt] = a[pos1];
else {
while(tong[pos2])
++ pos2;
ans[++ cnt] = pos2;
++ pos2;
}
}
for(int i = 1; i <= n - 1; ++ i)
if(ans[i])
write(ans[i]), putchar(' ');
putchar('\n');
continue;
}
}
for(; pos1 <= n; ++ pos1) {
if(pos1 == ban)
continue;
if(a[pos1] == 0) {
while(tong[pos2])
++ pos2;
ans[++ cnt] = pos2;
++ pos2;
continue;
}
else {
if(ban) {
ans[++ cnt] = a[pos1];
continue;
}
if(a[pos1] > a[pos1 + 1] && a[pos1 + 1]) {
tong[a[pos1]] = 0;
ban = 1;
continue;
}
while(tong[pos3])
++ pos3;
if(a[pos1] > a[pos1 + 1] && (!a[pos1 + 1]) && pos3 < a[pos1]) {
tong[a[pos1]] = 0;
ban = 1;
continue;
}
if(a[pos1] > a[pos1 + 1] && (!a[pos1 + 1])) {
ban = tong[pos2];
tong[pos2] = 0;
ans[++ cnt] = a[pos1];
continue;
}
ans[++ cnt] = a[pos1];
}
}
for(int i = 1; i < n; ++ i)
if(ans[i])
write(ans[i]), putchar(' ');
putchar('\n');
}
}
T3 斯坦纳树
虚树,现在还没学,不会,以后再说(以后就不说了)
粘一下官方题解:
先考虑所有边权都不为0的情况应该怎么做。
首先不难看出,点集在树上的斯坦纳树即为其虚树。虚树上的边权和即为正确答案。
我们下面证明:该算法是正确的,当且仅当给定点集 V1V1 在 GG 上的虚树点集 V′1V'1 和 V1V1 相同。(另一个说法是,任意 V1V1 中的两个点 u,vu,v 的 LCALCA 也在 V1V1 中)
其充分性不难证明,下面证其必要性:
若存在属于虚树点集 V′1V'1,且不属于 V1V1 的点(下面简称虚点),则在虚树上,虚点 uu 至少存在三条相关联的边。在斯坦纳树中,这三条边对答案的贡献是这三条边的权值和;而在我们建立的最小生成树中,至少有一条边的权值被计算了两次(由于虚点并不在原先的点集中,所以要让这三条边的另一个端点联通,就不得不经过其中一条边两次),因而答案就是错误的。
对于边权存在 00 的情况,只需要将所有由 00 边连结的连通块视为一个点,一个连通块被选中当且仅当其中至少有一个点被选中,然后应用上述做法即可。
关于如何维护虚点:可以考虑倒着做,每次删去一个点。如果删去的这个点有三条及以上的边相连,它就转型成为虚点。如果某次删除后,某个虚点只有两条边相连,则该虚点消失。答案为 11 当且仅当不存在虚点时。
T4 直径
最难改的一道,不改了,摆
首先意识到,所有直径的中点都是同一个点(当长度为偶数时),或者中边是同一条边(长度为奇数时)。不妨令这个点为根(长度为奇数时,额外在边的中间添加一个点,令为根)。
此时有根树无根树一一对应(因为对于每棵树,指定的点是唯一的)
下面以长度为偶数为例,如何计算直径条数?假设直径长度为k,则叶子节点的深度至多为 k2k2(令根节点深度为 00),我们将深度为 k2k2 的叶子称为有效叶子。
不难看出,任意一对不属于根节点的同一棵子树的有效叶子会产生一条直径。
此时,可以令 Fi,j,kFi,j,k 表示根节点的前若干个子树已经拥有了 ii 个节点,jj 个有效叶子,和 kk 条直径时的答案。
考虑一棵子树有效的信息只有两个,节点个数有效叶子个数,令这两个的值为 (x,y)(x,y)(下文中称为子树形态)。转移时按顺序枚举 (x,y)(x,y),并更新答案(注,上述写法有滚动数组的思想在,真正意义上,应该令 Fp,i,j,kFp,i,j,k,表示已经考虑前 pp 种子树形态(不同的 (x,y)(x,y))时的答案,然后让 FpFp 从 Fp−1Fp−1 处转移)。
为此,我们还需要用同样的技巧预处理一个数组,令 fi,j,kfi,j,k 表示一棵子树,拥有 ii 个节点,深度最深的点为 jj,深度最深的点恰有 kk 个时的方案数。同样枚举每一种子树形态对答案的贡献并更新。
直径长度为奇数时,需要额外处理一件事:子树必须恰好为两棵(因为我们加入的额外节点只连结中心边的两个端点)。一个简单的做法是增加一维表示选取了几棵子树。
9.12 CSP 模拟 36 (rk 7)
T1 博弈
第一眼我还以为是真博弈论,,,
我们先来研究,在数组 SS 确定的情况下,先手必胜的条件。
首先,当 SS 的最小值只有奇数个的时候,显然先手必胜。
当 SS 的最小值出现偶数次的时候,先手就不能第一个取最小值了,那么就可以把所有的最小值去掉,然后继续研究先手的必胜策略。
最后我们得到结论:先手必胜,当且仅当数组 SS 中出现至少一个数,满足它的出现次数为奇数。
所以考虑怎么枚举能保证时间复杂度。
值域有点大,先离散化一下。
记数组 SS 的值为数组内各个元素的异或和。如果存在两个数组值相同,或者说异或起来为 00,那么先手必败的情况增加一个。
想到哈希 + map+ map。
不难了。
赛时的数据是不卡的,赛后加数据了,只能用 unsigned long longunsigned long long,挺无语的。
因为异或哈希重在二进制位数,如果取模,二进制位数最多也就 3030 几位,容易被卡。
unsigned long longunsigned long long 却是 6060 多位。
嗯,没了。
代码
点击查看代码
#include <bits/stdc++.h>
#define int unsigned long long
#define M 1000005
#define P pair<int, int>
#define MP make_pair
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, m, q, T, a[M], b[M], w[M], ne[M], len, ans, has[M];
vector<P> edge[M];
unordered_map<int, int> mp;
void dfs1(int u, int fu, int cur) {
for(int i = 0; i < edge[u].size(); ++ i) {
int v = edge[u][i].first, ww = edge[u][i].second;
if(v == fu)
continue;
int ji = cur ^ has[ww];
ans += mp[ji];
++ mp[ji];
dfs1(v, u, ji);
}
}
signed main() {
T = read();
has[0] = 10007 * 13331;
for(int i = 1; i <= 500000; ++ i)
has[i] = has[i - 1] * 13331;
for(int t = 1; t <= T; ++ t) {
n = read();
ans = 0;
mp.clear();
mp[0] = 1;
for(int i = 1; i <= n; ++ i)
edge[i].clear();
for(int i = 1; i < n; ++ i)
a[i] = read(), b[i] = read(), w[i] = read(), ne[i] = w[i];
stable_sort(ne + 1, ne + 1 + n);
len = unique(ne + 1, ne + 1 + n) - ne - 1;
for(int i = 1; i < n; ++ i) {
w[i] = lower_bound(ne + 1, ne + 1 + len, w[i]) - ne;
edge[b[i]].push_back(MP(a[i], w[i]));
edge[a[i]].push_back(MP(b[i], w[i]));
}
dfs1(1, 0, 0);
write(n * (n - 1) / 2 - ans);
putchar('\n');
}
}
/*
3
5
1 2 2
1 3 1
3 4 1
3 5 2
5
1 2 0
2 3 2
3 4 2
4 5 0
5
1 2 0
1 3 1
3 4 0
3 5 2
9
8
10
*/
T2 跳跃
好好好,终于调出来了
这题有点恶心人
考虑怎么贪心让他最后更优。
用脚后跟思考一下可知,最后一定是在一个较大子段反复横跳。
但是它不一定第一次就能到那个子段的最右端,因为有一个限制条件是任意时刻分数不为负。
于是它会先在前面的某一段进行“原始积累”,积累够了跳过去。
然后它怎么积累呢?也是在一个较大段反复横跳。
于是又回到了我们上面那个问题。
设 dpidpi 代表它一开始从 11 跳到 ii 并且面向左边的最小步数。fifi 为它在这最小步数下所含有的“原始积累”。
于是就能转移了。
不想写了自己看代码去吧,一堆分类讨论,感觉这个题不值当我写那么多来解释,看一看想一想就通了
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 5005
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, T, k, a[M], dp[M], qian[M], ans, f[M], minn[M], p;
bool fail[M];
signed main() {
T = read();
for(int t = 1; t <= T; ++ t) {
n = read();
k = read();
ans = 0;
if(!k) {
putchar('0');
putchar('\n');
continue;
}
for(int i = 1; i <= n; ++ i) {
a[i] = read();
fail[i] = 0;
qian[i] = qian[i - 1] + a[i];
minn[i] = min(minn[i - 1], qian[i]);
}
if(a[1] < 0)
fail[1] = 1;
else {
ans = a[1] * k;
dp[1] = 1;
f[1] = a[1];
}
for(int i = 2; i <= n; ++ i) {
dp[i] = LONG_LONG_MAX;
if(qian[i] >= 0) {
dp[i] = 1;
f[i] = qian[i];
ans = max(ans, f[i] + (qian[i] - minn[i]) * (k - 1));
continue;
}
if(qian[i] - minn[i] <= 0) {
fail[i] = 1;
continue;
}
for(int j = 1; j < i; ++ j) {
if(fail[j])
continue;
if(qian[j] - minn[j])
p = max((int)0, (int)ceil((- (qian[i] - qian[j]) - f[j]) * 1.000 / (qian[j] - minn[j])));
else
continue;
if(p & 1)
++ p;
if(!p)
continue;
if(dp[i] < p + dp[j])
continue;
if(dp[i] == p + dp[j])
f[i] = max(f[i], f[j] + qian[i] - qian[j] + p * (qian[j] - minn[j]));
else {
dp[i] = p + dp[j];
f[i] = f[j] + qian[i] - qian[j] + p * (qian[j] - minn[j]);
}
}
if(dp[i] == LONG_LONG_MAX)
fail[i] = 1;
else
if(dp[i] <= k)
ans = max(ans, f[i] + (qian[i] - minn[i]) * (k - dp[i]));
}
write(ans);
putchar('\n');
}
}
/*
3
3 4
10 -100 80
15 400000000
-100 1 2 3 4 5 6 7 8 9 10 11 12 13 14
2 10
-77179 -79869
90
41999999900
0
*/
/*
1
8 1
-38933 57436 -79373 93885 -49015 1499 49128 -58979
*/
T3 大陆
其实考场非常疑惑为什么给个 104104 的数据范围,导致我对我的 O(n)O(n) 做法犹豫的半天。
两个限制条件:
-
省的大小都在 [B,3×B][B,3×B] 里。
-
对于一个省,这个省里面的任意一个城市 uu 到该省省会的简单路径上的每一个城市 vv,都要处于同一个省。
简单想一下就知道,只要一个省里面所有的城市都在一个块里面,然后在这个块内或与这个块之间相连的点,都可以是省会。
于是直接 dfsdfs,用栈维护一下。
每次够了 BB 个城市,就弹出。
最后剩下的城市,放到最新建成的一个省里即可。
时间复杂度 O(n)O(n),空间复杂度 O(n)O(n)。
听说有人被卡空间了,反正我空间 O(n)O(n),卡不到我头上
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 1000005
#define mod 1000000007
#define P pair<int, int>
#define MP make_pair
#define time() chrono::system_clock::now().time_since_epoch().count()
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, m, q, T, B, cnt, b[M], cap[M], size[M];
vector<int> edge[M], city, son[M];
void dfs(int u, int fa) {
for(int i = 0; i < edge[u].size(); ++ i) {
int v = edge[u][i];
if(v == fa)
continue;
dfs(v, u);
size[u] += size[v];
city.push_back(v);
if(size[u] >= B) {
++ cnt;
cap[cnt] = u;
while(size[u]) {
int s = city.back();
city.pop_back();
b[s] = cnt;
-- size[u];
}
}
}
size[u] += 1;
}
signed main() {
n = read();
B = read();
for(int i = 1; i < n ; ++ i) {
int u = read(), v = read();
edge[u].push_back(v);
edge[v].push_back(u);
}
dfs(1, 0);
city.push_back(1);
if(!cnt) {
++ cnt;
cap[cnt] = 1;
}
while(!city.empty()) {
int s = city.back();
city.pop_back();
b[s] = cnt;
}
write(cnt);
putchar('\n');
for(int i = 1; i <= n; ++ i) {
write(b[i]);
putchar(' ');
}
putchar('\n');
for(int i = 1; i <= cnt; ++ i) {
write(cap[i]);
putchar(' ');
}
}
T4 排列
平衡树,忘完了,不想改。
考虑用 fhq−treapfhq−treap 在线处理这个问题。区间循环右移就是 splitsplit 一下再换个顺序 $merge¥,关键是维护答案。
我们先考虑一个弱化的问题:询问全局是否存在二元上升子序列。
对于这个问题,在平衡树的每个节点里维护一个 tagtag,意义是该节点为根的子树内是否存在二元上升子序列,同时维护 maxnmaxn 和 minnminn ,表示子树最大/最小值。这样,push_uppush_up 的时候,就可以根据左右子树的信息,来维护 tagtag。
问题回到全局是否存在三元上升子序列。
还是维护 tagtag,定义为子树内是否存在三元上升子序列。注意到合并子树的时候,左子树或者右子树可能贡献两个元素作为三元上升子序列的一部分,所以只维护 maxnmaxn 和 minnminn 不行了。
我们可以多维护两个 maxn′maxn′ 和 minn′minn′。maxn′maxn′ 的定义是,子树里最大的元素值 valval,满足子树内存在一个以 valval 结尾的二元下降子序列;类似地定义 minn′minn′ 为子树里最小的元素值 valval,满足子树内存在一个以 valval 结尾的二元上升子序列。
有了 maxn′maxn′ 和 minn′minn′ ,就可以进行 tagtag 的维护了。
问题是,maxn′maxn′ 和 minn′minn′ 似乎无法维护。
下面我们仅关注 maxn′maxn′ 的维护,minn′minn′ 的维护是一样的。
首先 maxn′=max(maxn′ls,maxn′rs)maxn′=max(maxn′ls,maxn′rs) 这是显然的,问题是可能出现一个在左,一个在右的情况。
显然右边的那个点的值就是 maxnrsmaxnrs,而左边的点的值应该是最大的小于 maxnrsmaxnrs 的值。
看上去平衡树应该可以直接维护这个东西,但是我们的平衡树要支持区间循环右移,所以一开始就是按照 rsrs (位置)排序的。
我们想一下,如果当前子树的 tagtag 已经为 11 了,那么我们现在就不需要维护 maxn′maxn′ 了,因为答案肯定是 YESYES。
也就是说我们只用在子树 tagtag 为 00 的情况下,维护它的 maxnmaxn,根据 tagtag 的定义,子树内不存在三元上升子序列。
所以,左子树里小于 maxnrsmaxnrs 的元素单调递减。
所以只要找到左子树里最左侧的小于 maxnrsmaxnrs 的元素,类似地还要找右子树最右侧的大于 minnlsminnls 的元素。
这两个查找和常规平衡树的查排名(给定排名求值)是一样的,期望复杂度都是树高即 O(logn)O(logn)。
所以总复杂度 O(nlog2n)O(nlog2n)。
9.13 CSP 模拟 37 (rk 10)
T1 喷泉
(场切)
没啥可讲的,计算几何,但是极其简单。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 1000005
#define mod 1000000007
#define P pair<int, int>
#define MP make_pair
#define time() chrono::system_clock::now().time_since_epoch().count()
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
long double a, b, c, d, e, f, r, d1, d2, d3, d4, p, T, S;
signed main() {
T = read();
for(int t = 1; t <= T; ++ t) {
a = read();
b = read();
c = read();
d = read();
e = read();
f = read();
r = read();
d1 = sqrt((e - a) * (e - a) + (f - b) * (f - b));
d2 = sqrt((e - c) * (e - c) + (f - d) * (f - d));
d3 = sqrt((a - c) * (a - c) + (b - d) * (b - d));
p = (d1 + d2 + d3) / 2;
S = sqrt(p * (p - d1) * (p - d2) * (p - d3));
if(S)
d4 = S * 2 / d3;
else
d4 = min(d1, d2);
printf("%.2Lf %.2Lf\n", d4 - r, max(d1, d2) + r);
}
}
/*
2
0 0 100000 0 100000 100000 0
0 0 1 0 2 0 0
1.00 2.00
100000 141421.36
*/
/*
4
1 1 1 4 4 4 0
11 1 1 4 9 8 0
11 4 1 4 9 8 1
91665 81788 66905 42038 75347 76904 2844
3.00 4.24
6.13 8.94
3.00 9.94
8424.51 38717.46
*/
T2 红绿灯
ii 从 11 枚举到 nn。
如果每一个 ii 都跑一遍所有红绿灯的话会 TT。
考虑怎么剪枝。
发现如果当前时间为 tt,当前红绿灯为 ajaj,那么过这个红绿灯的时间为 ⌈taj⌉×aj⌈taj⌉×aj。
那么对于一些相邻的 ii,可能只用跑一遍,于是我们记录一下上一个 ii 经过每一个红绿灯的时间,如果 ii 与 i−1i−1 存在某一个红绿灯,他们通过这个红绿灯的时间相同,那么后面就不用跑了,breakbreak 掉就好了。
能拿 7373 了。
但是还不够,为什么呢?
我们发现,当 ajaj 一直都很小时,我们还是需要跑很多遍。
于是我们算一下整个数列的 lcmlcm,只跑 1∼lcm1∼lcm 里面的 ii,后面大于 lcmlcm 的可以直接利用前面小于 lcmlcm 的算。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 200005
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, m, q, T, a[M], lcm, tim[M], ans[M];
set<int> s[M];
signed main() {
n = read();
m = read();
if(m == 0) {
for(int i = 1; i <= n; ++ i)
write(i), putchar(' ');
return 0;
}
for(int i = 1; i <= m; ++ i)
a[i] = read();
lcm = a[m];
for(int i = m - 1; i >= 1; -- i)
lcm = lcm * a[i] / __gcd(lcm, a[i]);
if(lcm < 0)
lcm = INT_MAX;
for(int i = 1; i <= min(lcm, n); ++ i) {
int t = i;
for(int j = 1; j <= m; ++ j) {
t = ceil(t * 1.00 / a[j]) * a[j];
if(t == tim[j])
break;
tim[j] = t;
}
for(int j = 0; j * lcm + i <= n; ++ j)
ans[j * lcm + i] = tim[m] + j * lcm;
}
for(int i = 1; i <= n; ++ i)
write(ans[i]), putchar(' ');
}
T3 子集
有一个结论:
考虑去证明它。
当 k=1k=1 时,显然成立。
若其对于 1∼k−11∼k−1 都成立,那么有:
上式相当于从 SS 集合里选出一个 CC 集合,然后对于属于 SS 且不属于 CC 的元素,可选可不选,这些元素再与 CC 集合并,构成 TT。
于是:
证毕。
因此我们现在要做的就是:对每个和为 MM 的子集 CC,计算其贡献 k|S|−|C|k|S|−|C|,最后加到一起。
设 fi,wfi,w 代表原集合前 ii 项所有和为 ww 的子集的贡献和。
考虑怎么去转移。
-
如果我们不选 aiai,那么 |S||S| 加一,即乘上了 kk。
-
如果我们选 aiai,那么 |S||S| 和 |C||C| 都加一,没有变化。
于是 fi,w=k⋅fi−1,w+fi,w−aifi,w=k⋅fi−1,w+fi,w−ai
转移就好说了。
时间复杂度 O(nM)O(nM),空间复杂度 O(n2)O(n2),但也可以是 O(n)O(n),如果你愿意的话。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 5005
#define mod 1000000007
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, m, k, T, a[M], dp[M][M];
signed main() {
T = read();
for(int t = 1; t <= T; ++ t) {
n = read();
m = read();
k = read();
dp[0][0] = 1;
for(int i = 1; i <= n; ++ i) {
a[i] = read();
for(int j = 0; j <= m; ++ j) {
dp[i][j] = dp[i - 1][j] * k % mod;
if(j >= a[i])
dp[i][j] = (dp[i][j] + dp[i - 1][j - a[i]]) % mod;
}
}
write(dp[n][m]);
putchar('\n');
}
}
T4 佛怒火莲
逆天题。
所以我用逆天做法凑过去了。
暴力 dp +dp + 二分 ++ 随机化。正解 dpdp 没看懂。
假设一共有 colcol 种(颜色不同的)火焰,那么我们给这里的每一种 randrand 一个小于等于 kk 的数值出来,然后在新 randrand 的颜色上 dpdp。因为 kk 比较小,所以每次有 k!kkk!kk 的正确率。(约等于 0.03840.0384)。
于是我们 rand200rand200 次,这小正确率不就上来了吗(0.99960.9996)。
然后再讲 dpdp 和二分。
kk 很小,压个状态。
设 dpidpi 为当前状态为 ii 是已选火焰的最大值的最小值,就是说,在选了的这些火焰里,调一个最大的出来,让最大的最小。
设现在 checkcheck 的数为 midmid。
那么就对于二进制下任意为 00 的一位 jj,直接在颜色为 jj 的火焰里找最小的大于 dpi+middpi+mid 的火焰,设它的不稳定值为 bb,那么 dpi|2j=min(dpi|2j,b)dpi|2j=min(dpi|2j,b)。
如果最后 dp2k−1dp2k−1 不为极大值(我们一开始赋的),那么就存在 midmid,那么 ans=max(mid,ans)ans=max(mid,ans)。
最后输出一下 ansans 就行。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 10005
#define time() chrono::system_clock::now().time_since_epoch().count()
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, k, tp, T, a[M], b[M], c[M], col, r[M], dp[70], minn, maxn, bin[7] = {0, 1, 2, 4, 8, 16, 32}, ans;
vector<int> vec[M];
bool tong[M];
signed main() {
srand(time() / 1000 % 114514);
default_random_engine e;
e.seed(rand() * rand() % 1919810);
T = read();
for(int t = 1; t <= T; ++ t) {
n = read();
k = read();
tp = read();
ans = 0;
col = 0;
maxn = 0;
minn = INT_MAX;
for(int i = 1; i <= n; ++ i) {
a[i] = read();
b[i] = read();
maxn = max(maxn, b[i]);
minn = min(minn, b[i]);
if(!tong[a[i]]) {
c[++ col] = a[i];
tong[a[i]] = 1;
}
}
uniform_int_distribution<int> Z1(1, k);
for(int p = 1; p <= 2000000 / n; ++ p) {
for(int i = 1; i <= col; ++ i)
r[c[i]] = Z1(e);
for(int i = 1; i <= n; ++ i)
vec[r[a[i]]].push_back(b[i]);
for(int i = 1; i <= k; ++ i)
stable_sort(vec[i].begin(), vec[i].end());
int l = 0, r = maxn - minn;
while(l <= r) {
int mid = (l + r) >> 1;
for(int i = 1; i < bin[k + 1]; ++ i)
dp[i] = INT_MAX;
dp[0] = -mid;
for(int i = 0; i < bin[k + 1] - 1; ++ i) {
for(int j = 1; j <= k; ++ j) {
if(!(bin[j] & i)) {
auto pos = lower_bound(vec[j].begin(), vec[j].end(), dp[i] + mid);
if(pos != vec[j].end())
dp[i | bin[j]] = min(*pos, dp[i | bin[j]]);
}
}
}
if(dp[bin[k + 1] - 1] == INT_MAX)
r = mid - 1;
else {
l = mid + 1;
ans = max(ans, mid);
}
}
for(int i = 1; i <= k; ++ i)
vec[i].clear();
}
write(ans);
putchar('\n');
for(int i = 1; i <= n; ++ i)
tong[a[i]] = 0;
}
}
/*
1
5 3 1
1 1
1 2
3 3
2 4
2 5
2
*/
9.14 CSP 模拟 38 (rk 4)
T1 我是 A 题
乱搞的。
既然是随机生成,那么考虑乱搞。
观察生成器可知,有 1919 的概率使 ui=Aui=A 并且 vi=Bvi=B。
所以我们先跑一遍 nn,找出满足上述条件的最大的 wiwi,设为 maxnmaxn,然后算一下 ansans。
然后枚举 maxn+1∼Cmaxn+1∼C,再对于每一个 cc 枚举 1∼A1∼A,求出当前 a,ca,c 下最大的 vivi。每次算一下 ansans。
大概算一下就会知道我们枚举 cc 的次数不会多。
于是我跑出了最优解。
代码
点击查看代码
#include <bits/stdc++.h>
#define I __int128
#define P pair<int, int>
#define MP make_pair
using namespace std;
void write(I x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
const int N = 3e7 + 2;
typedef unsigned long long ull;
int n, A, B, C, u[N], v[N], w[N], tong[N], maxn, umaxn;
I ans;
ull Rand (ull &k1, ull &k2) {
ull k3 = k1, k4 = k2;
k1 = k4;
k3 ^= (k3 << 23);
k2 = k3 ^ k4 ^ (k3 >> 17) ^ (k4 >> 26);
return k2 + k4;
}
void GetData () {
ull x, y;
cin >> n >> A >> B >> C >> x >> y;
for (int i = 1; i <= n; i++) {
u[i] = Rand(x, y) % A + 1;
v[i] = Rand(x, y) % B + 1;
w[i] = Rand(x, y) % C + 1;
if (Rand(x, y) % 3 == 0) u[i] = A;
if (Rand(x, y) % 3 == 0) v[i] = B;
if ((u[i] != A) && (v[i] != B)) w[i] = C;
}
}
signed main() {
GetData();
for(int i = 1; i <= n; ++ i) {
if(u[i] == A && v[i] == B)
maxn = max(maxn, w[i]);
}
ans = (I)A * B * maxn;
for(int i = C; i >= maxn + 1; -- i) {
for(int j = 1; j <= n; ++ j)
if(w[j] == i)
tong[u[j]] = max(tong[u[j]], v[j]);
umaxn = 0;
for(int j = A; j >= 1; -- j) {
umaxn = max(umaxn, tong[j]);
ans += umaxn;
}
}
write(ans);
}
/*
2 10 10 10 114514 1919810
693
*/
/*
100 99 98 97 114514 1919810
941094
*/
T2 我是 B题
常见题型,但是我还是没看出来,,,
按说看到排名一类的东西就应该知道怎么设 dpdp 了,但是我确实没看出来。
对于 1∼n1∼n 里的每一个数我们跑一遍 dpdp。
设 dpi,jdpi,j 为已经删掉了 ii 个数,当前枚举的数 xx 的排名为 jj 的概率。
考虑怎么去转移。
能够想到,dpi,jdpi,j 可以由 dpi−1,jdpi−1,j 和 dpi−1,j+1dpi−1,j+1 转移过来。
但是我们发现,dpdp 系数与各个位置被删掉的概率有关。
于是设 fi,jfi,j 代表第 ii 道工序,排名为 jj 的数被删掉的概率,qiani,jqiani,j 为 fi,jfi,j 的前缀和,houi,jhoui,j 为 fi,jfi,j 的后缀和。
那么就能转移了。
从 dpi−1,j+1dpi−1,j+1 到 dpi,jdpi,j 意味着当前枚举的数前面有一个数被删掉了,那么就要乘上 qiani,jqiani,j。
从 dpi−1,jdpi−1,j 到 dpi,jdpi,j 意味着当前枚举的数后面有一个数被删掉了,那么就要乘上 houi,j+1houi,j+1。
完事了。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 305
#define mod 1000000007
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, p[M], cnt, bin[33], f[M][M], ans, dp[M][M], qian[M][M], hou[M][M];
signed main() {
n = read();
for(int i = 1; i < n; ++ i)
p[i] = read();
for(int i = 1; i < n; ++ i) {
int base = 1;
for(int j = 1; j <= n - i; ++ j)
f[i][j] = base * p[i] % mod, base = (1 - p[i] + mod) % mod * base % mod, qian[i][j] = (qian[i][j - 1] + f[i][j]) % mod;
f[i][n - i + 1] = base;
qian[i][n - i + 1] = (qian[i][n - i] + f[i][n - i + 1]) % mod;
for(int j = n - i + 1; j >= 1; -- j)
hou[i][j] = (hou[i][j + 1] + f[i][j]) % mod;
}
for(int x = 1; x <= n; ++ x) {
memset(dp, 0, sizeof(dp));
dp[0][x] = 1;
for(int i = 1; i < n; ++ i) {
for(int j = 1; j <= min(x, n - i); ++ j)
dp[i][j] = ((dp[i - 1][j + 1] * qian[i][j] % mod) + (dp[i - 1][j] * hou[i][j + 1] % mod)) % mod;
}
ans = (ans + dp[n - 1][1] * x % mod) % mod;
}
write(ans);
}
/*
10
1 1 1 0 0 0 1 1 1
7
*/
/*
10
3122 53425 1234214 653455 32412 1234 2432 3421 3543132
*/
/*
3
500000004 500000004
500000005
*/
T3 我是 C题
考场暴力最高分,7575,所以先来讲讲怎么打的暴力。
看完题发现,如果所有的 bibi 都相等的话,能二分,并且也有二分的部分分,于是想到假二分搞一搞。
然后还有暴力,毕竟暴力的正确性比二分高,所以考虑暴力和假二分混合一下(测试点分治)。
(顺便一提,不用嫌麻烦,其实二分就是一个二分的框架套了个暴力,所以复制粘贴一下暴力就好了)
考虑怎么暴力。
发现给了两档暴力部分分,O(n2k)O(n2k) 的和 O(nk)O(nk)。
O(n2k)O(n2k) 的很好想,就是枚举每一个 kk,然后再去枚举区间,每个区间再跑一遍。
去想怎么优化。
我们发现 n2n2 的复杂的出现在枚举区间那里,所以很容易想到先搞出最开始的区间,然后直接一整个区间往右走。过程中维护一些东西就好了。不再赘述。
考场暴力代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 1000005
#define mod 1000000007
#define P pair<int, int>
#define MP make_pair
#define time() chrono::system_clock::now().time_since_epoch().count()
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, a[M], b[M], tong[M], cnt, zong;
bool flag;
signed main() {
// freopen("medium_example.in", "r", stdin);
// freopen("out.out", "w", stdout);
n = read();
for(int i = 1; i <= n; ++ i)
a[i] = read();
for(int i = 1; i <= n; ++ i)
b[i] = read();
if(n <= 10000) {
for(int i = n; i >= 1; -- i) {
cnt = 0;
zong = 0;
for(int j = 1; j <= i; ++ j) {
++ tong[a[j]];
if(tong[a[j]] == b[i])
++ cnt;
if(tong[a[j]] == 1)
++ zong;
}
if(cnt == zong) {
write(i);
return 0;
}
for(int j = i + 1; j <= n; ++ j) {
++ tong[a[j]];
if(tong[a[j]] == b[i])
++ cnt;
if(tong[a[j]] == 1)
++ zong;
-- tong[a[j - i]];
if(tong[a[j - i]] == b[i] - 1)
-- cnt;
if(!tong[a[j - i]])
-- zong;
if(cnt == zong) {
write(i);
return 0;
}
}
for(int j = n; j >= n - i + 1; -- j)
-- tong[a[j]];
}
putchar('0');
return 0;
}
else {
int l = 1, r = n, ans = 0;
while(l <= r) {
int mid = (l + r) >> 1;
cnt = 0;
zong = 0;
flag = 0;
for(int j = 1; j <= mid; ++ j) {
++ tong[a[j]];
if(tong[a[j]] == b[mid])
++ cnt;
if(tong[a[j]] == 1)
++ zong;
}
if(cnt == zong)
flag = 1;
for(int j = mid + 1; j <= n; ++ j) {
if(flag) {
for(int k = j - 1; k >= j - 1 - mid + 1; -- k)
-- tong[a[k]];
break;
}
++ tong[a[j]];
if(tong[a[j]] == b[mid])
++ cnt;
if(tong[a[j]] == 1)
++ zong;
-- tong[a[j - mid]];
if(tong[a[j - mid]] == b[mid] - 1)
-- cnt;
if(!tong[a[j - mid]])
-- zong;
if(cnt == zong)
flag = 1;
}
if(flag) {
ans = mid;
l = mid + 1;
}
else {
r = mid - 1;
for(int j = n; j >= n - mid + 1; -- j)
-- tong[a[j]];
}
}
write(ans);
}
}
/*
10
2 1 1 2 0 2 3 1 2 0
4 3 3 2 2 2 2 2 2 2
4
*/
来讲一下正解,但是我懒得没打。
观察到 bb 是具有单调性的,如果一个数字 pp 导致一个区间不可行,那么这个区间所有包含 pp 的子区间必然也不可行。
考虑分治,f(l,r)f(l,r) 表示当前分治到 [l,r][l,r],那么我们把其中所有出现次数小于 br−l+1br−l+1 的数字全部删掉,区间就变成了若干段,分别递归处理即可。如果当前区间没有删掉任何数字,那么说明当前区间就
是合法的,直接更新答案。
考虑这个做法的复杂度,即考虑递归层数。不难发现当删除数字的时候递归层数才会增加,而最多有种 √(n)√(n) 不同的出现次数,因此这个做法的复杂度为 O(n√(n))O(n√(n))。
考虑优化上面的做法。首先一次找出所有出现次数较小的数字比较耗时,我们可以找到任意一个出现次数不满足要求的数字,并且从这个数字处把区间分成两半进行递归,并不会影响正确性,还大大降低了编程复杂度。
但是由于分治两边可能不均等,会导致时间复杂度退化成 O(n2)O(n2),但如果我们在 min(len[l,mid],len[mid+1,r])min(len[l,mid],len[mid+1,r]) 的时间内完成当前层的操作,复杂度就是启发式合并的复杂度,即 O(nlogn)O(nlogn)。
于是利用类似于 meet in the middle 的思想,我们从区间两端同时找出现次数不满足要求的数字,显然寻找的时间就是 min(len[l,mid],len[mid+1,r])min(len[l,mid],len[mid+1,r]) 了。
但是我们还需要维护每个数字的出现次数。考虑 cntcnt 记 数组为每个数字的出现次数,并且假定每次递归时 cntcnt 数组恰好是当前区间中所有数字的出现次数,并且返回时清空 cntcnt 数组。
把拆开的两区间中较大的称为大区间,较小的称为小区间,每次找到分割点后,先把小区间中所有数字从 cntcnt 当中去掉,就可以直接递归大区间了。之后 cntcnt 数组被清空,我们只需要再遍历一遍小区间,把所有数字加到 cntcnt 中,递归完毕后 cntcnt 也恰好被清空,符合返回要求。
特殊的,如果一个区间找不到分割点,则需要遍历整个区间清空 cntcnt,复杂度显然没有问题。初始时,cntcnt 必须设置为每个数字的出现次数再进行递归。
由于每一步花费的复杂度都是 min(len[l,mid],len[mid+1,r])min(len[l,mid],len[mid+1,r]),因此总复杂度为 O(nlogn)O(nlogn)。
T4 我是 D 题
据说非常难改,维护 99 个东西的线段树。
没改。
首先进行一步转化,极长连续段的平方等于满足 xi=xi+1=⋯=xjxi=xi+1=⋯=xj 的数对 (i,j)(i,j) 的数量。期望即为所有 (i, j),满足 xi=xi+1=⋯=xjxi=xi+1=⋯=xj 的概率和。
-
当 (i,j)(i,j) 中有两个及以上的不同的颜色时, p(i,j)=0p(i,j)=0。
-
当 (i,j)(i,j) 中有且仅有一种确定的颜色时,p(i,j)=m−[i,j]中未确定的数的数量p(i,j)=m−[i,j]中未确定的数的数量。
-
当 (i,j)(i,j) 中没有确定的颜色时,p(i,j)=m−(j−i)p(i,j)=m−(j−i)。
于是只需要求 n∑i=1n∑j=i+1p(i,j)n∑i=1n∑j=i+1p(i,j)。
我们发现这个信息是容易合并的,只需要维护一下东西:
-
该区间内概率的和;
-
左边第一个确定的颜色;
-
右边第一个确定的颜色;
-
区间长度;
-
区间中未确定的数的数量;
-
[L,i]/[i,R][L,i]/[i,R] 中有且仅有一种确定的颜色时 // 没有确定的颜色时 m−[i,j]中未确定的数的数量m−[i,j]中未确定的数的数量 的和。
时间复杂度 O(nlogn)O(nlogn)。
9.19 CSP 模拟 39 联测 1 (rk 12)
今天好像有他校的学生和我们一起考
T1 序列问题
场切,但是其实考场上降智了老半天。
其实就是要找一个对于所有 i<ji<j,都有 ai<ajai<aj 且 i−ai≤i−aji−ai≤i−aj 的最长子序列。
当时考场想的是建一个二维平面,横坐标表示 aiai,纵坐标表示 i−aii−ai,fi,jfi,j 表示以 ii 为横坐标,以 jj 为纵坐标的值。
那么有 fi,j=maxx<i,y≤jfx,yfi,j=maxx<i,y≤jfx,y
所以考场上想到了二维树状数组,但是发现是求最值,就想二维线段树或二维 ST 表什么的,然后一个小时没了。
然后我突然想到,哦对,有个东西叫 sortsort。
然后就会了,按照 aiai 的值排个序,然后找最长不下降子序列。时间复杂度 O(nlogn)O(nlogn)。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 500005
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, m, a[M], ans, maxn[M << 2];
pair<int, int> pa[M];
map<int, int> mp;
bool tong[M];
int ask(int rt, int l, int r, int zuo, int you) {
if(zuo <= l && r <= you)
return maxn[rt];
int res = 0, mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
if(zuo <= mid)
res = ask(ls, l, mid, zuo, you);
if(you > mid)
res = max(res, ask(rs, mid + 1, r, zuo, you));
return res;
}
void add(int rt, int l, int r, int pos, int addend) {
if(l == r) {
maxn[rt] = addend;
return;
}
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
if(pos <= mid)
add(ls, l, mid, pos, addend);
else
add(rs, mid + 1, r, pos, addend);
maxn[rt] = max(maxn[ls], maxn[rs]);
}
inline bool cmp(pair<int, int> a, pair<int, int> b) {
if(a.first != b.first)
return a.first < b.first;
else
return a.second > b.second;
}
signed main() {
freopen("sequence.in", "r", stdin);
freopen("sequence.out", "w", stdout);
n = read();
for(int i = 1; i <= n; ++ i)
pa[i].first = read(), pa[i].second = i - pa[i].first;
// stable_sort(pa + 1, pa + 1 + n);
stable_sort(pa + 1, pa + 1 + n, cmp);
// cout << "???" << endl;
for(int i = 1; i <= n; ++ i) {
if(pa[i].first > n)
break;
// if(pa[i].second < 0 || tong[pa[i].first])
if(pa[i].second < 0)
continue;
int ji = ask(1, 0, n, 0, pa[i].second);
ans = max(ans, ji + 1);
// cout << i << " " << ans << endl;
add(1, 0, n, pa[i].second, ji + 1);
// tong[pa[i].first] = 1;
}
write(ans);
}
/*
9
2 3 3 5 5 6 7 7 8
*/
T2 钱仓
赛时连样例都膜不出来,直接寄,但凡打个暴力排名就能进前三。
正解还是挺有意思的,感觉不太好想,反正不属于我。
赛时觉得所有放了 1mol1mol 钱的地方都不用动了,但是赛后发现也得动,因为它可以减少其他地方的花费,比如 2 1 1 02 1 1 0,把 22 移到 00 花费 99,但是一个一个移只需要 33。
那么我们发现,我们可以找到一个终点,然后从终点往起点枚举,如果是零就压进队列,不是的话就将队列里的 00 弹出并算贡献,如果当前点减为了 00,那么再将当前点压进去。
但是,因为它是个环,所以有可能不合法,就比如上面那个例子,给它转一转,转成 0 2 1 10 2 1 1,那么我们就用不了刚才的方法了,因为从终点枚举,有一时刻的钱仓不为 11 且被我们放过去了,所以根本不合法。所以 O(n2)O(n2) 暴力枚举终点的方法实际上有很多是不合法的。
容易发现,所有合法的情况的答案都是一样的,所以我们只需要找到一个即可。
怎么找呢?
最大子段和知道吧,好的你会了。
其实就是找最大子段和,然后让最大子段和的起点作为我们枚举的起点,终点也就知道了。
但是在哪个数组上找最大子段和呢?反正不可能是原数组,否则起点一定是 11。
我们需要重新构造一个数组,每一位置上的数设置为原数 −1−1。
为什么这么构造?因为我们后面是一个进队出队的过程,进队相当于 −1−1,出队相当于 +1+1。我们需要让某一段在队内的数量最少,这样的话后面的部分都会被前面的部分捣出队,就不会出现原先不合法的情况。
实现就很简单了。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 1000005
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, m, q, T, a[M], b[M], maxn, cur, curl, l, ans;
deque<int> dq;
signed main() {
freopen("barn.in", "r", stdin);
freopen("barn.out", "w", stdout);
n = read();
for (int i = 1; i <= n; ++ i) {
a[i] = read();
b[i] = a[i] - 1;
b[i + n] = b[i];
}
a[0] = a[n];
curl = 1;
for(int i = 1; i <= 2 * n; ++ i) {
cur += b[i];
if(cur < 0) {
cur = 0;
curl = i + 1;
}
if(cur > maxn && curl <= n) {
maxn = cur;
l = curl;
}
}
cerr << "l = " << l << endl;
for(int i = 1; i <= n; ++ i)
b[i] = a[(l + i - 1) % n];//, cout << i << " " << b[i] << endl;
for(int i = n; i >= 1; -- i) {
while(dq.size() && b[i]) {
ans += (dq.front() - i) * (dq.front() - i);
dq.pop_front();
-- b[i];
}
if(!b[i])
dq.push_back(i);
}
write(ans);
}
T3 自然数
线段树好题。mexmex 好题。
先去想 mexmex 有什么性质:
-
当我们算出了一个集合的 mexmex,如果再向这个集合里加数,那么新集合的 mexmex 一定大于等于原来的值。也就是单调不降。
-
我们删去了一个可重集里一个数,若可重集中还有相同的数,那么 mexmex 不变。
-
我们删去可重集中的一个数 aa,若可重集中没有相同的数,那么 mex=min(a,mex)mex=min(a,mex)
这三个性质显而易见。
考虑有了这些我们能得到什么。
我们先去算左端点为 11,右端点为 1∼n1∼n 的 mexmex,存起来,并维护一下每一个数下一次出现的位置,存在 nexnex 里。
然后我们取从左往右枚举左端点。
当左端点加一时,集合中删去一个数。
考虑删掉这个数对哪些值有影响。
根据第三个性质,我们发现它的影响范围只有 l∼nexl−1l∼nexl−1。也就意味着 mexl∼mexnexl−1mexl∼mexnexl−1 都要与 alal 取 minmin。
吉司机线段树?不不不,还有第一条性质。
因为它有单调性,所以我们可以直接用区间赋值,不用吉司机线段树,吧、普通线段树即可。
基本讲完了,每一次枚举左端点时,算一下 n∑i=lmexin∑i=lmexi 即可。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 200005
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, a[M], ans, pos[M], mex[M], nex[M], minn[M << 2], sum[M << 2], lazy[M << 2];
bool tong[M];
void push_down(int rt, int l, int r) {
if(lazy[rt] >= 0) {
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
sum[ls] = lazy[rt] * (mid - l + 1);
sum[rs] = lazy[rt] * (r - (mid + 1) + 1);
minn[ls] = lazy[rt];
minn[rs] = lazy[rt];
lazy[ls] = lazy[rt];
lazy[rs] = lazy[rt];
lazy[rt] = -1;
}
}
void build(int rt, int l, int r) {
if(l == r) {
minn[rt] = sum[rt] = mex[l];
return;
}
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
build(ls, l, mid);
build(rs, mid + 1, r);
minn[rt] = min(minn[ls], minn[rs]);
sum[rt] = sum[ls] + sum[rs];
}
void change(int rt, int l, int r, int zuo, int you, int addend) {
// cout << rt << " " << l << " " << r << " " << zuo << " " << you << " " << addend << " " << minn[rt] << " " << sum[rt] << endl;
if(zuo <= l && r <= you && minn[rt] >= addend) {
minn[rt] = addend;
sum[rt] = (r - l + 1) * addend;
lazy[rt] = addend;
return;
}
if(l == r)
return;
push_down(rt, l, r);
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
if(zuo <= mid && minn[rs] > addend)
change(ls, l, mid, zuo, you, addend);
if(you > mid)
change(rs, mid + 1, r, zuo, you, addend);
minn[rt] = min(minn[ls], minn[rs]);
sum[rt] = sum[ls] + sum[rs];
// cout << rt << " " << l << " " << r << " " << zuo << " " << you << " " << addend << " " << minn[rt] << " " << sum[rt] << endl;
}
int ask(int rt, int l, int r, int zuo, int you) {
if(zuo <= l && r <= you)
return sum[rt];
push_down(rt, l, r);
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1, res = 0;
if(zuo <= mid)
res += ask(ls, l, mid, zuo, you);
if(you > mid)
res += ask(rs, mid + 1, r, zuo, you);
minn[rt] = min(minn[ls], minn[rs]);
sum[rt] = sum[ls] + sum[rs];
return res;
}
signed main() {
// freopen("sample_mex2.in", "r", stdin);
// freopen("out.out", "w", stdout);
freopen("mex.in", "r", stdin);
freopen("mex.out", "w", stdout);
n = read();
memset(lazy, -1, sizeof(lazy));
for(int i = 1; i <= n; ++ i) {
a[i] = read();
mex[i] = mex[i - 1];
if(a[i] > n)
continue;
if(!tong[a[i]])
tong[a[i]] = 1;
else
nex[pos[a[i]]] = i;
pos[a[i]] = i;
while(tong[mex[i]])
++ mex[i];
// cout << i << " " << mex[i] << endl;
}
build(1, 1, n);
ans = sum[1];
// cout << 0 << " ans = " << ans << endl;
for(int i = 1; i < n; ++ i) {
if(!nex[i])
change(1, 1, n, i + 1, n, a[i]);//, cout << "???";
else
if(i + 1 <= nex[i] - 1)
change(1, 1, n, i + 1, nex[i] - 1, a[i]);
ans += ask(1, 1, n, i + 1, n);
// cout << i << " ans = " << ans << endl;
}
write(ans);
}
// freopen("sample_mex3.in", "r", stdin);
// freopen("out.out", "w", stdout);
/*
5
2 2 5 1 0
11
*/
/*
5
0 0 2 3 7
9
*/
T4 环路
感觉挺可写的。
矩阵加,矩阵乘,反正就是一道矩阵题。
把 YY 看成 11,NN 看成 00。设 S1S1 为初始矩阵,SiSi 为 Si1Si1。
所以我们要求的就是 S1+S2+⋯+Sk−1S1+S2+⋯+Sk−1。
这个题比较智障,是小于 kk,所以我们后面用 kk 代表 k−1k−1,所以我们要求 S1+S2+⋯+SkS1+S2+⋯+Sk
但是 n3⋅kn3⋅k 明显过不去,考虑优化。
我们想尽量把 kk 优化成 logklogk,所以考虑能不能直接一半一半减掉当前处理的情况。
答案是能的。
我们要求 S1+S2+⋯+SkS1+S2+⋯+Sk,即 k∑i=1Sik∑i=1Si,可以看看它除以 22 以后等于什么。(接下来的除法都向下取整)
-
当 kk 为奇数时,原式等于 k2∑i=1Si+Sk2⋅k2∑i=1+Skk2∑i=1Si+Sk2⋅k2∑i=1+Sk
-
当 kk 为偶数时,原式等于 k2∑i=1Si+Sk2⋅k2∑i=1k2∑i=1Si+Sk2⋅k2∑i=1
我们如果只是一层一层往下去算 k∑i=1Sik∑i=1Si 是好算的,但是中间穿插着 Sk2Sk2 和 SkSk,就比较麻烦,所以我们考虑在过程中记录一下这些东西。
当最后算到 1∑i=1Si1∑i=1Si 时,我们可以让一个矩阵 stagingstaging 等于初始矩阵 S1S1,在回溯上一层时,我们会计算 1∑i=1Si+S1⋅1∑i=11∑i=1Si+S1⋅1∑i=1,即 S1+S2S1+S2,或 1∑i=1Si+S1⋅1∑i=1+S31∑i=1Si+S1⋅1∑i=1+S3,即 S1+S2+S3S1+S2+S3。我们在这假设 res1=1∑i=1Si,res2=staging⋅1∑i=1Sires1=1∑i=1Si,res2=staging⋅1∑i=1Si,即 S2S2
-
对于第一种情况,我们让 staging=staging∗stagingstaging=staging∗staging,然后直接返回 res1+res2res1+res2,这样我们下一次用到的 stagingstaging 就是 S2S2。
-
对于第二种情况,我们让 staging=staging∗staging∗S1staging=staging∗staging∗S1,此时 staging=S3staging=S3,然后返回 res1+res2+stagingres1+res2+staging,我们下一次用到的 stagingstaging 就是 S3S3。
类比一下,我上面说的处理情况就是对于所有 kk 为奇数或偶数的情况,所以就可以写了。时间复杂度 O(n3logk)O(n3logk)。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 105
using namespace std;
struct Matrix {
int a[M][M];
int width;
int heigth;
};
int n, m, mod, sum;
char ch;
Matrix base, staging;
Matrix operator + (Matrix x, Matrix y) {
for(int i = 1; i <= x.heigth; ++ i)
for(int j = 1; j <= x.width; ++ j)
x.a[i][j] = (x.a[i][j] + y.a[i][j]) % mod;
return x;
}
Matrix operator * (Matrix x, Matrix y) {
Matrix tran;
tran.heigth = x.heigth;
tran.width = y.width;
memset(tran.a, 0, sizeof(tran.a));
for(int i = 1; i <= tran.heigth; ++ i)
for(int j = 1; j <= tran.width; ++ j)
for(int k = 1; k <= x.width; ++ k)
tran.a[i][j] = (tran.a[i][j] + x.a[i][k] * y.a[k][j] % mod) % mod;
return tran;
}
Matrix solve(int cur) {
if(cur == 1) {
staging = base;
return base;
}
Matrix res1 = solve(cur / 2);
Matrix res2 = res1 * staging;
staging = staging * staging;
if(cur & 1) {
staging = staging * base;
return res1 + res2 + staging;
}
else
return res1 + res2;
}
signed main() {
freopen("tour.in","r",stdin);
freopen("tour.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
base.heigth = n;
base.width = n;
for(int i = 1; i <= n; ++ i) {
for(int j = 1; j <= n; ++ j) {
cin >> ch;
base.a[i][j] = ch == 'Y' ? 1 : 0;
}
}
cin >> m >> mod;
Matrix ans = solve(m - 1);
for(int i = 1; i <= n; ++ i)
sum = (sum + ans.a[i][i]) % mod;
cout << sum;
}
9.20 CSP 模拟 40 联测 2 (rk 10)
这场太 sb 了。总结一下就是 T1 卡 DFSDFS,T2 树链剖分板子,T3 线段树分治板子,T4 假思路能过样例,所以好多人以为假思路是正解,下考场直接懵逼。
T1 ^_^
非常 sb 的一道题,1e71e7 但是 O(n)O(n) 过不去。
卡 DFSDFS 的 sb 题我是真第一次见。
思路就是 CSP 模拟 24 的 T2T2 原题,自己找去 算了我还是讲一下吧。
思路没什么难的。
根据期望的可加性,我们只需要算出每个点被选中的期望,然后相加即可。
考虑怎么算一个点被选中的期望。
一个点被选中,当且仅当在这之前没有选中过它子树中的任何一个点,即,它是它的子树中第一个被选中的点,又因为它必须要被染色,所以一个点 uu 被染色的期望就是 1sizeu1sizeu。
时间复杂度 O(n)O(n)。
但是 DFSDFS 被卡了,需要用一个性质。
因为他说,对于点 u,fau<uu,fau<u,所以可以直接从 n∼1n∼1 枚举,就过了。
赛后改了时限,我的 DFSDFS 仍旧过不了。。。
因为还有一个地方被卡了,边数过多不能用 vectorvector,会寄。
DFS 代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define mod 998244353
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int ans, n, size[10000005], inv[10000005];
vector<int> edge[10000005];
void dfs(int u) {
size[u] = 1;
for(int i = 0; i < edge[u].size(); ++ i) {
dfs(edge[u][i]);
size[u] += size[edge[u][i]];
}
ans = ans + inv[size[u]];
}
signed main() {
n = read();
inv[1] = 1;
for(int i = 2; i <= n; ++ i)
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
for(int i = 2; i <= n; ++ i)
edge[read()].push_back(i);
dfs(1);
write(ans % mod);
}
循环代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define mod 998244353
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int ans, n, size[10000005], inv[10000005], fa[10000005];
signed main() {
n = read();
inv[1] = 1;
for(int i = 2; i <= n; ++ i)
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
size[1] = 1;
for(int i = 2; i <= n; ++ i)
fa[i] = read(), size[i] = 1;
for(int i = n; i >= 1; -- i) {
size[fa[i]] += size[i];
ans += inv[size[i]];
}
write(ans % mod);
}
T2 文件包管理器
场切。
在 tg.OJ 的学校训练 -> 衡中 2021 级高中 -> 树链剖分里有,自己找去。
非常简单,就是略微有点不好调。
树上操作,一眼树链剖分。
维护什么也很好想,就是维护一个区间内 11 的数量(11 代表已安装,00 代表未安装),直接加和就行,很好 push_uppush_up。
-
安装一个,就求从这个点到根结点路径上 00 的数量,就是它的深度减去这条路径上 11 的数量。
-
删除一个,就求这个这个点的子树里的 11 的数量。
时间复杂度 O(nlog2n)O(nlog2n)
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 1000005
#define mod 1000000007
#define P pair<int, int>
#define MP make_pair
#define time() chrono::system_clock::now().time_since_epoch().count()
using namespace std;
int n, q, fa[M], deep[M], siz[M], son[M], tim, dfn[M], tran[M], top[M], sum[M], lazy[M], las[M];
vector<int> edge[M];
string s;
void dfs1(int u) {
deep[u] = deep[fa[u]] + 1;
siz[u] = 1;
int cur = 0;
for(int i = 0; i < edge[u].size(); ++ i) {
int v = edge[u][i];
dfs1(v);
siz[u] += siz[v];
if(siz[v] > cur) {
cur = siz[v];
son[u] = v;
}
}
}
void dfs2(int u, bool op) {
dfn[u] = ++ tim;
las[u] = tim;
tran[tim] = u;
if(!op)
top[u] = u;
else
top[u] = top[fa[u]];
if(!son[u])
return;
dfs2(son[u], 1);
las[u] = las[son[u]];
for(int i = 0; i < edge[u].size(); ++ i) {
int v = edge[u][i];
if(v == son[u])
continue;
dfs2(v, 0);
las[u] = las[v];
}
}
inline void push_down(int rt, int l, int r) {
if(lazy[rt] >= 0) {
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
sum[ls] = lazy[rt] * (mid - l + 1);
sum[rs] = lazy[rt] * (r - (mid + 1) + 1);
lazy[ls] = lazy[rt];
lazy[rs] = lazy[rt];
lazy[rt] = -1;
}
}
int ask_sum(int rt, int l, int r, int zuo, int you) {
// cout << rt << " " << l << " " << r << " " << zuo << " " << you << " " << sum[rt] << endl;
if(zuo <= l && r <= you)
return sum[rt];
push_down(rt, l, r);
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1, res = 0;
if(zuo <= mid)
res = ask_sum(ls, l, mid, zuo, you);
if(you > mid)
res += ask_sum(rs, mid + 1, r, zuo , you);
sum[rt] = sum[ls] + sum[rs];
return res;
}
int ask(int rt, int l, int r, int pos) {
if(l == r)
return sum[rt];
push_down(rt, l, r);
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
sum[rt] = sum[ls] + sum[rs];
if(pos <= mid)
return ask(ls, l, mid, pos);
else
return ask(rs, mid + 1, r, pos);
}
void change(int rt, int l, int r, int zuo, int you, int addend) {
if(zuo <= l && r <= you) {
lazy[rt] = addend;
sum[rt] = addend * (r - l + 1);
return;
}
push_down(rt, l, r);
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
if(zuo <= mid)
change(ls, l, mid, zuo, you, addend);
if(you > mid)
change(rs, mid + 1, r, zuo , you, addend);
sum[rt] = sum[ls] + sum[rs];
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
memset(lazy, -1, sizeof(lazy));
cin >> n;
for(int i = 2; i <= n; ++ i) {
cin >> fa[i];
++ fa[i];
edge[fa[i]].push_back(i);
}
dfs1(1);
dfs2(1, 0);
cin >> q;
for(int i = 1; i <= q; ++ i) {
int x;
cin >> s >> x;
++ x;
if(s[0] == 'i') {
if(ask(1, 1, n, dfn[x])) {
cout << "0\n";
continue;
}
int ans = 0, ji = x;
while(top[x] != 1) {
ans += ask_sum(1, 1, n, dfn[top[x]], dfn[x]);
change(1, 1, n, dfn[top[x]], dfn[x], 1);
x = fa[top[x]];
}
ans += ask_sum(1, 1, n, 1, dfn[x]);
change(1, 1, n, 1, dfn[x], 1);
cout << deep[ji] - ans << '\n';
}
else {
if(!ask(1, 1, n, dfn[x])) {
cout << "0\n";
continue;
}
cout << ask_sum(1, 1, n, dfn[x], las[x]) << '\n';
change(1, 1, n, dfn[x], las[x], 0);
}
}
}
T3 地理课
如果你会线段树分治,那么这道题还是比较简单的。
如果你不会,这道题也是可做的。
线段树维护各个边存在的时间,然后直接对线段树 DFSDFS,用并查集维护一下各个块的大小。
但是不能路径压缩,只能启发式合并,因为有删除操作。
到了每一层都记录下我们在这一层加了哪些边,退出这一层的时候直接还回来就行。
加边是 O(logn)O(logn) 的,删边是 O(1)O(1) 的。
每条边都会被加一次删一次,基本上是 O(nlogn)O(nlogn) 的,常数不小。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 1000005
#define P pair<int, int>
#define MP make_pair
#define mod 1000000007
using namespace std;
int n, m, fa[M], size[M], ans = 1, inv[M];
map<pair<int, int>, int> mp;
vector<P> tree[M << 2];
vector<pair<P, P> > st;
inline int find(int x) {
while(fa[x] != x)
x = fa[x];
return x;
}
void dfs(int rt, int l, int r) {
int pos = st.size();
for(int i = 0; i < tree[rt].size(); ++ i) {
int x = tree[rt][i].first, y = tree[rt][i].second;
int fx = find(x), fy = find(y);
if(fx == fy)
continue;
if(size[fx] > size[fy])
swap(fx, fy);
st.push_back(MP(MP(fx, size[fx]), MP(fy, size[fy])));
ans = ans * inv[size[fx]] % mod * inv[size[fy]] % mod * (size[fx] + size[fy]) % mod;
size[fy] += size[fx];
fa[fx] = fy;
}
if(l == r)
cout << ans << '\n';
else {
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
dfs(ls, l, mid);
dfs(rs, mid + 1, r);
}
while(st.size() != pos) {
pair<P, P> pa = st.back();
ans = ans * inv[pa.first.second + pa.second.second] % mod * pa.first.second % mod * pa.second.second % mod;
fa[pa.first.first] = pa.first.first;
size[pa.second.first] = pa.second.second;
st.pop_back();
}
}
void add(int rt, int l, int r, int zuo, int you, P pa) {
if(zuo <= l && r <= you) {
tree[rt].push_back(pa);
return;
}
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
if(zuo <= mid)
add(ls, l, mid, zuo, you, pa);
if(you > mid)
add(rs, mid + 1, r, zuo, you, pa);
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
inv[1] = 1;
for(int i = 2; i <= n; ++ i)
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
for(int i = 1; i <= n; ++ i)
fa[i] = i, size[i] = 1;
for(int i = 1; i <= m; ++ i) {
int op, u, v;
cin >> op >> u >> v;
if(op == 1) {
mp[MP(u, v)] = i;
mp[MP(v, u)] = i;
}
else {
add(1, 1, m, mp[MP(u, v)], i - 1, MP(u, v));
mp[MP(u, v)] = 0;
mp[MP(v, u)] = 0;
}
}
for(auto it = mp.begin(); it != mp.end(); ++ it)
if(it -> second)
add(1, 1, m, it -> second, m, it -> first);
dfs(1, 1, m);
}
/*
5 6
1 1 3
1 2 3
1 1 2
1 4 5
1 3 4
2 3 4
2
3
3
6
5
6
*/
T4 道路和航线
赛时以为 Dij 可以处理负权不能处理负环,但是显然连负权也不能处理,所以我寄了。
只有航线是有负数的,道路只有正数。
并且航线保证不会构成环。
于是我们看作很多个块,每个块内的点由道路相连,块与块间通过航线相连。
块内可以用 Dij,块间可以用拓扑。
于是就能做了。
代码
点击查看代码
#include <bits/stdc++.h>
#define M 200005
#define int long long
#define MP make_pair
using namespace std;
int dis[M], T ,R, P, S, belong[M], cnt, in[M];
vector<pair<int, int>> edge[M], plane[M];
vector<int> incl[M];
bool vis[M];
bool dfs(int u, int cur) {
if(belong[u])
return 0;
belong[u] = cur;
incl[cur].push_back(u);
for(int i = 0; i < edge[u].size(); ++ i)
dfs(edge[u][i].second, cur);
return 1;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> T >> R >> P >> S;
for(int i = 1; i <= T; ++ i)
dis[i] = LONG_LONG_MAX;
for(int i = 1; i <= R; ++ i) {
int u, v, w;
cin >> u >> v >> w;
edge[u].push_back(make_pair(w, v));
edge[v].push_back(make_pair(w, u));
}
for(int i = 1; i <= T; ++ i)
if(!belong[i])
dfs(i, ++ cnt);
for(int i = 1; i <= P; ++ i) {
int u, v, w;
cin >> u >> v >> w;
plane[u].push_back(make_pair(w, v));
++ in[belong[v]];
}
dis[S] = 0;
queue<int> Q;
for(int i = 1; i <= cnt; ++ i)
if(!in[i])
Q.push(i);
while(!Q.empty()) {
// cout << "!!!" << endl;
int tp = Q.front();
Q.pop();
priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int>>> q;
for(int i = 0; i < incl[tp].size(); ++ i) {
// cout << incl[tp][i] << " " << "@@@" << endl;
if(dis[incl[tp][i]] != LONG_LONG_MAX)
q.push(MP(dis[incl[tp][i]], incl[tp][i]));//, cout << incl[tp][i] << " " << "###" << endl;;
}
while(!q.empty()) {
// cout << "???" << endl;
int u = q.top().second;
q.pop();
if(vis[u])
continue;
vis[u] = 1;
for(int i = 0; i < edge[u].size(); ++ i) {
int v = edge[u][i].second, w = edge[u][i].first;
if(dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
if(!vis[v])
q.push(make_pair(dis[v], v));
}
}
for(int i = 0; i < plane[u].size(); ++ i)
dis[plane[u][i].second] = min(dis[plane[u][i].second], plane[u][i].first + dis[u]);
}
for(int i = 0; i < incl[tp].size(); ++ i) {
for(int j = 0; j < plane[incl[tp][i]].size(); ++ j) {
-- in[belong[plane[incl[tp][i]][j].second]];
if(!in[belong[plane[incl[tp][i]][j].second]])
Q.push(belong[plane[incl[tp][i]][j].second]);
}
}
}
for(int i = 1; i <= T; ++ i) {
if(dis[i] != LONG_LONG_MAX)
cout << dis[i] << '\n';
else
cout << "NO PATH\n";
}
}
9.21 CSP 模拟 41 联测 3 (rk 30)
T1 开挂
考场降智,最后没排序,挂了 3636,直接 rk 30。。。
需要加的次数是一定的,这个比较好想。
于是我们尽量让一个一直加,加到再加没有用了为止。因为这样就可以让其他的数加的次数少,而我们一会儿再让加的次数多的数的代价最小,就是最优方案。
实现比较简单。
代码
点击查看代码
#include <bits/stdc++.h>
#define M 1000005
#define int unsigned long long
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(unsigned long long x) {
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, a[M], b[M], c[M], cnt, pos[M];
unsigned long long ans;
unordered_map<int, int> mp;
signed main() {
n = read();
for(int i = 1; i <= n; ++ i)
a[i] = read();
for(int i = 1; i <= n; ++ i)
b[i] = read();
stable_sort(a + 1, a + 1 + n);
stable_sort(b + 1, b + 1 + n);
for(int i = n; i >= 1; -- i) {
if(mp[a[i]]) {
pos[i] = pos[i + 1] + 1;
while(mp[pos[i]])
pos[i] = pos[mp[pos[i]]] + 1;
mp[pos[i]] = i;
c[++ cnt] = pos[i] - a[i];
mp[a[i]] = i;
}
else
pos[i] = a[i], mp[a[i]] = i;
// cout << i << ' ' << a[i] << ' ' << pos[i] << endl;
}
stable_sort(c + 1, c + 1 + cnt);
for(int i = cnt; i >= 1; -- i)
ans += (unsigned long long)c[i] * b[cnt - i + 1];//, cout << c[i] << " " << b[cnt - i + 1] << endl;;
write(ans);
}
/*
10
14 14 12 6 20 10 10 3 15 17
20 22 85 82 100 8 89 78 35 86
36
*/
T2 叁仟柒佰万
没看出来是 dp。
有一个性质,我们分隔出来的每一块都一定与整体的 值相同。
然后可以 dp 了。
dpidpi 表示以 ii 为右端点有多少种方案数,那么有 \diaplaystyledpi=∑ij=1dpj+dppos−1\diaplaystyledpi=∑ij=1dpj+dppos−1,pospos 为能使 [pos,i][pos,i] 的 mexmex 等于总体的 mexmex 的离 ii 最近的点。
时间复杂度不够,考虑前缀和优化。
dpidpi 表示以 ii 以及 ii 左面的点为右端点的方案数,那么 dpi=dpi−1+dppos−1dpi=dpi−1+dppos−1。
最后答案就是 dpn−dpn−1dpn−dpn−1,我看也有人写作 dppos−1dppos−1。
细节还是有点的。
代码
点击查看代码
#include <bits/stdc++.h>
#include <bits/stdc++.h>
#define M 37000001
#define mod 1000000007
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, m, pos, T, a[M], x, y, mex, dp[M], cnt[M], tot;
bool tong[M];
signed main() {
T = read();
for(int t = 1; t <= T; ++ t) {
pos = 0;
tot = 0;
n = read();
if(n == 37000000) {
x = read();
y = read();
a[1] = 0;
for(int i = 2; i <= n; ++ i)
a[i] = (a[i - 1] * x + y + i) & 262143;
}
else
for(int i = 1; i <= n; ++ i)
a[i] = read();
for(int i = 1; i <= n; ++ i)
tong[a[i]] = 1;
for(int i = 0; i <= n; ++ i) {
if(!tong[i]) {
mex = i;
break;
}
}
// cout << mex << "???" << endl;
dp[0] = 1;
for(int i = 1; i <= n; ++ i) {
dp[i] = dp[i - 1];
++ cnt[a[i]];
if(pos) {
while(pos < i && (cnt[a[pos]] > 1 || a[pos] > mex))
-- cnt[a[pos]], ++ pos;
dp[i] = (dp[i] + dp[pos - 1]) % mod;
}
else {
if(a[i] < mex && cnt[a[i]] == 1)
++ tot;
if(tot == mex) {
pos = 1;
dp[i] += 1;
}
}
// cout << i << " " << pos << " " << tot << " " << dp[i] << "???" << endl;
}
write((dp[n] - dp[n - 1] + mod) % mod);
putchar('\n');
if(t != T) {
memset(tong, 0, sizeof(tong));
memset(cnt, 0, sizeof(cnt));
}
}
}
T3 超级加倍
个人感觉这个题挺 nb 的,也可能是我之前没做过这类题的缘故。
好像叫重构树,没用过不知道,反正用起来觉得挺高级。
对于原树,我们考虑建两颗新树,一颗大根树,一颗小根树。
考虑怎么建。
只说大根树,小根树原理一样的。
先弄好并查集,然后从一到 nn 枚举每一个点,对于每一个点找与它相连的比它小的点,并建一条从比它小的点的祖先到当前点的边,并把比它小的点的祖先的父亲设为它。
就没了。
建完两棵树,在一棵树上跑 DFSDFS,标上 DFSDFS 序。
然后在另一科树上 DFSDFS。
对于当前 DFSDFS 到的点,找他到根节点的路径上有多少个点出现在另一棵树以它为根的子树里,统计答案并让总答案加上它的答案。我是用权值树状数组维护的。然后把他的 DFSDFS 序也加到树状数组里,去 DFSDFS 它儿子。最后回溯的时候再把它的 DFSDFS 序从树状数组里删掉就好了。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 2000005
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, ans, fa[M], fu, tim, l[M], r[M], tree[M];
int to1[M << 1], nex1[M << 1], cnt1, from1[M << 1], head1[M << 1];
int to2[M << 1], nex2[M << 1], cnt2, from2[M << 1], head2[M << 1];
int to3[M << 1], nex3[M << 1], cnt3, from3[M << 1], head3[M << 1];
inline void add1(int u, int v) {
nex1[++ cnt1] = head1[u];
head1[u] = cnt1;
from1[cnt1] = u;
to1[cnt1] = v;
}
inline void add2(int u, int v) {
nex2[++ cnt2] = head2[u];
head2[u] = cnt2;
from2[cnt2] = u;
to2[cnt2] = v;
}
inline void add3(int u, int v) {
nex3[++ cnt3] = head3[u];
head3[u] = cnt3;
from3[cnt3] = u;
to3[cnt3] = v;
}
inline int find(int x) {
while(x != fa[x])
x = fa[x] = fa[fa[x]];
return x;
}
inline int lowbit(int x) {return x & (-x);}
inline void add(int x, int addend) {
for(int i = x; i <= n; i += lowbit(i))
tree[i] += addend;
}
inline int ask(int x) {
// cout<<" wo wo o w "<<x<<endl;
int res = 0;
if(!x)
return 0;
for(int i = x; i; i -= lowbit(i))
res += tree[i];
return res;
}
void dfs2(int u, int f) {
l[u] = ++ tim;
for(int i = head2[u]; i; i = nex2[i])
if(to2[i] != f)
dfs2(to2[i], u);
r[u] = tim;
// cout << "???" << u << " " << f << endl;
}
void dfs3(int u, int f) {
// cout << u << " - - - -> "<<l[u]<<" "<<r[u]<<endl;
ans += ask(r[u]) - ask(l[u] - 1);
add(l[u], 1);
// cout << u << endl;
for(int i = head3[u]; i; i = nex3[i])
if(to3[i] != f)
dfs3(to3[i], u);
add(l[u], -1);
}
signed main() {
// freopen("sample_charity2.in", "r", stdin);
// freopen("out.out", "w", stdout);
n = read();
fu = read();
fa[1] = 1;
for(int i = 2; i <= n; ++ i) {
fu = read();
add1(i, fu);
add1(fu, i);
fa[i] = i;
}
for(int i = 1; i <= n; ++ i) {
for(int j = head1[i]; j; j = nex1[j]) {
if(to1[j] < i) {
// if(to1[])
fa[to1[j]] = find(to1[j]);
add2(i, fa[to1[j]]);
add2(fa[to1[j]], i);
fa[fa[to1[j]]] = i;
}
}
}
dfs2(n, 0);
for(int i = 1; i <= n; ++ i)
fa[i] = i;
for(int i = n; i >= 1; -- i) {
for(int j = head1[i]; j; j = nex1[j]) {
if(to1[j] > i) {
fa[to1[j]] = find(to1[j]);
add3(i, fa[to1[j]]);
add3(fa[to1[j]], i);
fa[fa[to1[j]]] = i;
}
}
}
// cout << " ???" << endl;
dfs3(1, 0);
// cout << " ???" << endl;
write(ans);
}
T4 欢乐豆
直接粘的题解,改了改 Markdown 炸了的地方(原题解所有用到 Markdown 的地方全是炸的)。我觉得题解写的狗屁不通,反正我读不懂。
对于原来的完全图,显然我们可以分成两种部分:
-
边权被修改过的点
-
边权没有被修改过的点
对于第⼆种情况,我们显然可以直接统计答案,即 (n−1)×∑ai(n−1)×∑ai,因为我到其他点的距离都是相等的,并且我直接⼀步到是最优的,所以我除去我⾃⼰,对答案的贡献就是这些。
那么处理完上边的点,我们现在就只剩 2⋅m2⋅m 个点了(⼀条边连着两个点)
因为我们要的是所有点对的最短距离,所以显然我们得给这些点全跑⼀遍 DijDij。
我们定义联通块是被修改过的边连的点所组成的图。这个联通块可能有好多,因为我不⼀定是连续修改的,我可以改了 1−>21−>2,改了 2−>32−>3,然后就去改 11−>1211−>12,那么显然他们是不联通的。
那么我们考虑⼀些情况。
如果我们更新联通块⾥边的点,显然,有两种情况,⼀种是直接⾛联通块⾥边的点,另⼀种是如果我的⼀条边被更改的很⼤,显然我可以去联通块外⼀个点然后再回来是更优秀的,那么这样我们现在所保留的 2⋅m2⋅m 个点是不够的,因为还需要⼀个外边的点。
考虑保留⼀个怎样的点。
因为对于现在要跑 DijDij 的点,它对于我联通块外的点的最短距离是相同的,它对于块外的点没被改过边,都是它的 auau,所以我只需要考虑⼀个点,让他回到联通块的距离最⼩,那么我⾛块外的距离就是最⼩的。
如果我们更新联通块外边的点,那么显然也有两种情况:
-
直接⾛我的 auau 边到它
-
⾛我联通块⾥边的点,然后再通过那个点(假设这个点为 jj)⾛出去
那么我们可以为了压缩 DijDij 的松弛时间,将优先队列⾥直接塞⼊ dis[j]+ajdis[j]+aj,这样我直接从优先队列⾥拿出来的第⼀个就是最优秀的松弛条件,那么⼀个点只需要被更新⼀次就 ok 了。
考虑松弛的过程。
对于⼀个点的出边,他可以将 [1,n][1,n] 分成若⼲段,对于端点处 (v1,v2,v3)(v1,v2,v3) 我可以直接⽤ dis[u]+a1/a2/a3dis[u]+a1/a2/a3 来更新(因为我现在使⽤ uu 更新,所以相对于它来说,最短距离就是这些,当然还会通过其他的去松弛,他是类似⼀层⼀层的)
对于两个端点之间所夹着的地⽅,显然是等价的,如果是⽤线段树来维护的话就可以直接区间修改了(维护最⼩值)。
如果是并查集的话就只需要在每个点被更新后将他指向它加⼀的位置即可,因为每个点只被更新一次,下次就不⽤更新了,可以直接跳,所以并查集可以做到让你⼀直跳需要更改的点。
9.22 CSP 模拟 42 联测 4 (rk 21)
9.23 CSP 模拟 43 联测 5 (rk 28)
这场太逆天,几乎没啥分差,难得要死
T1 数据恢复
有两个限制,一个是要保证一个点的父亲在这个点拿出来之前拿出来,另一个是要保证总贡献最大。
如果在树上进行操作,那么显然就是保证了第一个限制,然后去想怎么保证第二个限制。
没想出来。
所以考虑换成序列,先维护第二个限制。
设 vi=biaivi=biai,那么我们想要尽量先拿出 vivi 最大的,直接贪心,这样限制而就有保证了。优先队列维护就行了。
然后考虑怎么维护限制一。
既然要先拿父亲,那么即便它儿子的 vivi 大于它,也只能先拿父亲。所以在拿完父亲以后直接拿儿子是最优的。所以可以考虑如果优先队列先找到儿子,那么直接算出两者之间的贡献,再用并查集合并父亲和儿子,含义为拿完父亲要直接拿这个儿子。然后将两者的 a,b,va,b,v 合并算一个新的,重新压进队列里。
过程中可能会出现一个点在优先队列里出现好几次的情况,所以需要维护个 visvis 数组保证每个点只会被算一次。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 1000005
#define P pair<double, int>
#define MP make_pair
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
struct Node {
long double v;
int id;
int a;
int b;
};
bool operator < (Node a, Node b) {
return a.v < b.v;
}
int n, f[M], a[M], b[M], fa[M], tot, size[M], ans;
long double v[M];
bool vis[M];
vector<int> vec[M];
priority_queue<Node, vector<Node>, less<Node> > q;
inline int find(int x) {
while(x != fa[x])
x = fa[x] = fa[fa[x]];
return x;
}
void cl(int x) {
vis[x] = 1;
while(!vec[x].empty())
cl(vec[x].back()), vec[x].pop_back();
}
signed main() {
#ifdef ONLINE_JUDGE
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif
n = read();
fa[1] = 1;
for(int i = 2; i <= n; ++ i)
f[i] = read(), fa[i] = i;
for(int i = 1; i <= n; ++ i) {
a[i] = read();
b[i] = read();
tot += a[i];
v[i] = b[i] * 1.000 / a[i];
Node node = {v[i], i, a[i], b[i]};
q.push(node);
}
vis[0] = 1;
while(!q.empty()) {
Node d = q.top();
q.pop();
if(vis[d.id] || d.a != a[d.id] || d.b != b[d.id])
continue;
if(vis[f[d.id]]) {
tot -= a[d.id];
ans += (tot * b[d.id]);
cl(d.id);
// cout << "???" << d.id << " " << tot << " " << ans << endl;
}
else {
fa[f[d.id]] = find(f[d.id]);
// vis[d.id] = 1;
fa[d.id] = fa[f[d.id]];
vec[fa[d.id]].push_back(d.id);
ans += b[fa[d.id]] * a[d.id];
// cout << d.id << " " << fa[d.id] << " " << ans << endl;
a[fa[d.id]] += a[d.id];
b[fa[d.id]] += b[d.id];
v[fa[d.id]] = b[fa[d.id]] * 1.000 / a[fa[d.id]];
Node node = {v[fa[d.id]], fa[d.id], a[fa[d.id]], b[fa[d.id]]};
q.push(node);
}
}
write(ans);
}
/*
5
1 2 3 1
1 8
5 3
4 3
7 5
10 7
374
*/
T2 下落的小球(ball)
赛时其实已经基本想出来了,但是卡在了一个地方。
不太难,应该是实际意义上的 T1。
设几个东西:
-
sisi,以 ii 为根的子树的大小;
-
bibi,以 ii 为根子树中所有叶子的 aiai 的和。
-
riri,以 ii 为根的子树里的所有结点都被这个子树里的叶节点选中后,还能选中几个,或者写作 si−bisi−bi;
-
fifi,以 ii 为根的子树有多少种方案。
先来考虑一个简单的问题,如何判断方案是否存在。
显然,如果在每一步操作后,∀i,ri≥0∀i,ri≥0,那么该方案合法。
以上判断方式启发我们对 riri 进行思考。
我们认为 xixi 为使点 ii 从有球变成没球的那次操作。
如果考虑从子树往根做,对于点 ii,我们先假设以 ii 为根的子树内的 bibi 次操作直接的相对顺序已经排好了。此时我们想要计算 fifi。
对于点 ii,我们能发现以 ii 为根的子树中那 bibi 次操作不会影响第 ri+1ri+1 次操作。
换句话说,子树 ii 中那 bibi 次操作,前 riri 次操作并不会让对子树 ii 中点的状态受影响,而第 ri+1ri+1 次操作让 ii 从有球变成没球,而之后的操作只会改变子树 ii 中点的状态。
因为后 sisi 次操作只会影响子树中的状态,所以子树之间这部分的操作可以随意合并。
而前 riri 次操作都相当于更改子树外的,所以这一部分可以任意合并。
因为在子树外是所有操作都操作完后才能操作子树内的,所有前面那部分和后面那部分的顺序是不能更改的。
因此这题的解法很明显了,对于每一个子树的操作,可以把前 riri 次当成 AA 部分,把后 sisi 次当成 BB 部分。子树合并时,AA 部分与 AA 部分合并,BB 部分与 BB 部分合并,然后把合并完的 AA 部分接在 BB 部分前面。
至于计数,只需要用到一个式子:对于一个相对顺序确定的长度为 xx 的序列和一个相对顺序确定的长度为 yy 的序列,它们合并的方案数为 (x+yx)(x+yx)。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 1000005
#define mod 1000000007
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9')
ch = getchar();
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, m, a[M], jiecheng[M], inv[M], x, y, ans, r[M], s[M], nex[M], head[M], to[M], from[M], cnt;
inline int C(int a, int b) {
if(a < 0 || b < 0 || a - b < 0)
return 0;
return jiecheng[a] * inv[b] % mod * inv[a - b] % mod;
}
int dfs(int u) {
int res = 1;
if(!head[u]) {
r[u] = a[u] - 1;
s[u] = 1;
return 1;
}
for(int i = head[u]; i; i = nex[i]) {
int v = to[i];
res = res * dfs(v) % mod * C(r[u] + r[v], r[v]) % mod * C(s[u] + s[v], s[v]) % mod;
r[u] = r[u] + r[v];
s[u] = s[u] + s[v];
}
-- r[u];
++ s[u];
return res;
}
void exgcd(int a, int b, int &x, int &y) {
if(b == 0) {
x = 1;
y = 0;
return;
}
exgcd(b, a % b, x, y);
int z = x;
x = y;
y = z - a / b * y;
return;
}
inline int qiu(int m) {
exgcd(m, mod, x , y);
return (x % mod + mod) % mod;
}
signed main() {
#ifdef ONLINE_JUDGE
freopen("ball.in","r",stdin);
freopen("ball.out","w",stdout);
#endif
n = read();
jiecheng[0] = inv[0] = jiecheng[1] = inv[1] = 1;
for(int i = 2; i <= n; ++ i)
jiecheng[i] = jiecheng[i - 1] * i % mod, inv[i] = qiu(jiecheng[i]);
for(int i = 2; i <= n; ++ i) {
int fa = read();
nex[++ cnt] = head[fa];
head[fa] = cnt;
from[cnt] = fa;
to[cnt] = i;
}
for(int i = 1 ; i <= n; ++ i)
a[i] = read();
write(dfs(1));
}
/*
5
1 2 2 1
0 0 2 2 1
*/
T3 消失的运算符(operator)
没改。
感觉题解写的有点草,反正我直接粘的他的。
对于没有括号的:
设 fi,jfi,j 表示前 ii 个数字,已经有 jj 个加法。可以直接 dp,加个前缀和优化即可。
当然还有一个对正解有启发作用的解法。
可以设 Fi,jFi,j 表示前 ii 个数字,已经有 jj 个加法,不包括最后一段乘法的表达式的值,gi,jgi,j 表示最后一段乘法的值。
这两个 dp 互相转移即可。
那么我们再来考虑有括号的:
建出树,然后开始树上背包。
子树合并用刚刚的第二种做法。
T4 古老的序列问题(sequence)
基本同 Luogu P8868 [NOIP2022] 比赛,这道题没改,但是那道题我有空是一定会做的。
9.25 CSP 模拟 44 联测 6 (rk 18)
T1 最大匹配
贪心题,想到贪心,不会贪。
wi,jwi,j 可以简化为 abs(max(ai,bi)−min(aj,bj))abs(max(ai,bi)−min(aj,bj))。
我们可以把 maxmax 和 −min−min 分别看做贡献,那么对于一个数对 (ai,bi)(ai,bi),一定会产生 max(ai,bi)max(ai,bi) 或 −min(ai,bi)−min(ai,bi) 的贡献。
根据题意,有 nn 个数对产生 max(ai,bi)max(ai,bi) 的贡献,另外 nn 个产生 −min(ai,bi)−min(ai,bi) 的贡献。
我们可以先假设所有数都产生 max(ai,bi)max(ai,bi) 的贡献,我们要从中选 nn 个数让它们产生的贡献改变为 −min(ai,bi)−min(ai,bi),改变后相对于改变前一定是减少了的,且改变量为 max(ai,bi)−(−min(aj,bj))max(ai,bi)−(−min(aj,bj)) 即 ai+biai+bi。所以我们显然要选 ai+biai+bi
最小的 nn 个让它们改变贡献。按 ai+biai+bi 排序即可。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 100005
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, ans;
struct N {
int a;
int b;
int sum;
}m[M << 1];
inline bool cmp(N a, N b) {
return a.sum > b.sum;
}
signed main() {
n = read();
for(int i = 1; i <= 2 * n; ++ i) {
m[i].a = read();
m[i].b = read();
m[i].sum = m[i].a + m[i].b;
}
stable_sort(m + 1, m + 1 + n * 2, cmp);
for(int i = 1; i <= n; ++ i)
ans += max(m[i].a, m[i].b) - min(m[2 * n - i + 1].a, m[2 * n - i + 1].b);
write(ans);
}
T2 挑战ABC
赛时一直在扣这个题,结果有两个极其简单的地方写寄了。
本来想写写我赛时思路,但是写了 110110 以后发现太长了于是不想写了(当时已经写了 4040 多行)。
所以给你们放官方题解,不过代码还是我赛时代码。
不知道我赛时是什么精神状态,代码里没有一个数组,全是单变量,然后写了几十个变量,,,当时我写那份代码就没想着让后人看,,,
22 次操作⼀定可以:找到最短前缀使得某字符 XX 出现 nn 次,(最⼩保证其他字符出现 <n<n 次),剩下的按照需求分配就可以。
11 次操作可能可以:如果有 22 个 mxmx(出现次数大于 nn 的字符),⼀个 mimi(出现次数小于 nn 的字符),可以找到⼀个区间 [l,r][l,r] 使得 mx1mx1 出现 mx1cnt−nmx1cnt−n 次,mx2mx2 出现 mx2cnt−nmx2cnt−n 次,那么这⼀段刚好变成 mimi,双指针维护。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 1000005
using namespace std;
int n, ji1, re;
int cnta, cntb, cntc, flag1, flag2;
int cur, maxn, pos, poslast, curpos;
int it1 = 1, it2, cnt1, cnt2, ji2;
char sm, bi, mi;
string s;
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> s;
s = '#' + s + '#';
for(int i = 1; i <= 3 * n; ++ i) {
if(s[i] == 'A')
++ cnta;
if(s[i] == 'B')
++ cntb;
if(s[i] == 'C')
++ cntc;
}
if(cnta == n)
++ flag1;
if(cntb == n)
++ flag1;
if(cntc == n)
++ flag1;
if(flag1 == 3) {
cout << "0\n";
return 0;
}
if(cnta > n)
++ flag2;
if(cntb > n)
++ flag2;
if(cntc > n)
++ flag2;
if(cnta == min({cnta, cntb, cntc}))
sm = 'A';
if(cntb == min({cnta, cntb, cntc}))
sm = 'B';
if(cntc == min({cnta, cntb, cntc}))
sm = 'C';
if(cnta == max({cnta, cntb, cntc}))
bi = 'A';
if(cntb == max({cnta, cntb, cntc}))
bi = 'B';
if(cntc == max({cnta, cntb, cntc}))
bi = 'C';
for(int i = 0; i <= 2; ++ i)
if(sm != 'A' + i && bi != 'A' + i)
mi = 'A' + i;
ji1 = max({cnta, cntb, cntc}) - n;
// cerr << cnta << " " << cntb << " " << cntc << " " << bi << " " << mi << " " << sm << endl;
if(flag1 == 1) {
curpos = 1;
for(int i = 1; i <= 3 * n + 1; ++ i) {
if(s[i] == bi)
++ cur;
else {
if(s[i] == sm)
continue;
cur = 0;
curpos = i + 1;
}
if(cur == ji1) {
cout << 1 << endl;
cout << curpos << ' ' << i << ' ' << sm << endl;
return 0;
}
}
curpos = 1;
for(int i = 1; i <= 3 * n + 1; ++ i) {
if(s[i] == bi)
++ cur;
else {
if(s[i] == sm)
continue;
if(cur > maxn) {
maxn = cur;
pos = curpos;
poslast = i - 1;
}
cur = 0;
curpos = i + 1;
}
if(cur + maxn == ji1) {
cout << 2 << endl;
cout << pos << ' ' << poslast << ' ' << sm << endl;
cout << curpos << ' ' << i << ' ' << sm << endl;
return 0;
}
}
}
if(flag2 == 1) {
for(int i = 1; i <= 3 * n; ++ i) {
if(s[i] == bi)
++ cur;
s[i] = sm;
if(cur == ji1) {
cout << 2 << endl;
cout << 1 << ' ' << i << ' ' << sm << endl;
break;
}
}
for(int i = 1; i <= 3 * n; ++ i)
if(s[i] == sm)
++ re;
cout << 1 << ' ' << re - n << ' ' << mi << endl;
return 0;
}
if(flag2 == 2) {
ji2 = n - min({cnta, cntb, cntc}) - ji1;
for(int i = 1; i <= 3 * n; ++ i) {
if(s[i] == bi)
++ cnt1;
if(s[i] == mi)
++ cnt2;
// cout << i << " " << cnt1 << " " << cnt2 << " " << ji1 << " " << ji2 << " " << bi << " " << mi << endl;
if(cnt1 < ji1)
continue;
while(cnt1 > ji1) {
if(s[it1] == bi)
-- cnt1;
if(s[it1] == mi)
-- cnt2;
++ it1;
}
while(cnt2 > ji2 && s[it1] != bi) {
if(s[it1] == mi)
-- cnt2;
++ it1;
}
// cout << i << " " << it1 << " " << cnt1 << " " << cnt2 << endl;
if(cnt2 == ji2) {
cout << 1 << endl;
cout << it1 << " " << i << " " << sm;
return 0;
}
}
cnt1 = cnt2 = 0;
bool op;
for(int i = 1; i <= 3 * n; ++ i) {
if(s[i] == bi)
++ cnt1;
if(s[i] == mi)
++ cnt2;
s[i] = sm;
if(cnt1 == ji1 && cnt2 > ji2) {
cout << 2 << endl;
cout << 1 << " " << i << " " << sm << endl;
op = 1;
break;
}
if(cnt2 == ji2 && cnt1 > ji1) {
cout << 2 << endl;
cout << 1 << ' ' << i << " " << sm << endl;
op = 0;
break;
}
}
// cout << ji1 << " " << ji2 << " " << cnt1 << " " << cnt2 << " " << op << endl;
if(!op)
cout << 1 << " " << cnt1 - ji1 << ' ' << bi << endl;
else
cout << 1 << " " << cnt2 - ji2 << ' ' << mi << endl;
// cout << ":::" << endl;
return 0;
}
}
T3 三级跳
线段树好题,又一次被线段树薄纱成这样。
如果我们固定区间 [a,b][a,b],那么对于这个区间的最优解,一定有 vala+valbvala+valb 为这个区间的最大值。
所以有效的 a,ba,b 并不大。
所有有效的 a,ba,b 一定有 maxb−1i=a+1valimaxb−1i=a+1vali 小于 valavala 也小于 valbvalb。
所以跑单调栈预处理一下。
然后我们把 valavala 和 valbvalb 放到 valcvalc 上。说具体点就是,对于一个 a,ba,b,因为 b−a≤c−bb−a≤c−b,所以有 2×b−a≤c2×b−a≤c,所以直接对于大于等于 2×b−a2×b−a 的点,和当前的 vala+valb+valcvala+valb+valc 取个 maxmax,用线段树维护一下。
操作离线一下,按 ll 排个序。
从后往前扫,扫到操作就输出,扫到有效的 a,ba,b 就跑线段树。
时间复杂度 O(nlogn)O(nlogn)。
代码
点击查看代码
#include <bits/stdc++.h>
#define M 500005
// #define int long long
#define P pair<int, int>
#define MP make_pair
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, q, a[M], pos[M], cur = 1, st[M], top = 1, ans[M];
int maxn[M << 2], pmaxn[M << 2], lazy[M << 2];
vector<int> s[M];
pair<P, int> A[M];
inline bool cmp(pair<P, int> a, pair<P, int> b) {
return a.first.first > b.first.first;
}
inline void push_down(int rt) {
if(lazy[rt]) {
int ls = rt << 1, rs = ls | 1;
maxn[ls] = max(maxn[ls], lazy[rt] + pmaxn[ls]);
maxn[rs] = max(maxn[rs], lazy[rt] + pmaxn[rs]);
lazy[ls] = max(lazy[ls], lazy[rt]);
lazy[rs] = max(lazy[rs], lazy[rt]);
lazy[rt] = 0;
}
}
void build(int rt, int l, int r) {
if(l == r) {
pmaxn[rt] = a[l];
return;
}
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
build(ls, l, mid);
build(rs, mid + 1, r);
pmaxn[rt] = max(pmaxn[ls], pmaxn[rs]);
}
void add(int rt, int l, int r, int zuo, int you, int addend) {
if(zuo > you)
return;
if(zuo <= l && r <= you) {
maxn[rt] = max(maxn[rt], addend + pmaxn[rt]);
lazy[rt] = max(lazy[rt], addend);
return;
}
push_down(rt);
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1;
if(zuo <= mid)
add(ls, l, mid, zuo, you, addend);
if(you > mid)
add(rs, mid + 1, r, zuo, you, addend);
maxn[rt] = max(maxn[ls], maxn[rs]);
}
int ask(int rt, int l, int r, int zuo, int you) {
if(zuo <= l && r <= you)
return maxn[rt];
push_down(rt);
int mid = (l + r) >> 1, ls = rt << 1, rs = ls | 1, res = 0;
if(zuo <= mid)
res = ask(ls, l, mid, zuo, you);
if(you > mid)
res = max(res, ask(rs, mid + 1, r, zuo, you));
maxn[rt] = max(maxn[ls], maxn[rs]);
return res;
}
signed main() {
n = read();
for(int i = 1; i <= n; ++ i) {
a[i] = read();
while(top && a[i] >= st[top]) {
s[pos[top]].push_back(i);
-- top;
}
if(top)
s[pos[top]].push_back(i);
st[++ top] = a[i];
pos[top] = i;
}
q = read();
for(int i = 1; i <= q; ++ i)
A[i].first.first = read(), A[i].first.second = read(), A[i].second = i;
stable_sort(A + 1, A + 1 + q, cmp);
build(1, 1, n);
for(int i = n; i >= 1; -- i) {
for(int j = 0; j < s[i].size(); ++ j)
add(1, 1, n, 2 * s[i][j] - i, n, a[i] + a[s[i][j]]);
while(A[cur].first.first == i) {
ans[A[cur].second] = ask(1, 1, n, i + 2, A[cur].first.second);
++ cur;
}
if(cur == q + 1)
break;
}
for(int i = 1; i <= q; ++ i)
write(ans[i]), putchar('\n');
}
/*
20
66 70 75 87 25 80 27 5 5 16 15 70 74 47 1 45 13 2 2 15
1
7 16
*/
T4 经典线性基
不会,粘的学长的。
虽然正解WA了,但是思想是可以借鉴的。
【1】对于一段长区间的二进制分解,不重不漏:
for(int i=0;i<=62;++i) { if(l&(1ll<<i)&&l+(1ll<<i)-1<=r){Divid(l,l+(1ll<<i+1));l+=(1ll<<i);} }
for(int i=62;i>=0;--i) { if(l+(1ll<<i)-1<=r) {Divid(l,l+(1ll<<i)-1);l+=(1ll<<i);} }
注意正要求&(1<<i)==1,反着不要求
【2】长度范围小根号下范围筛质数
【3】极长区间蒙一个所有都能凑出
代码
也是学长的。
点击查看代码
//慎独,深思,毋躁,自律,专注
#include<bits/stdc++.h>
using namespace std;
#define _f(i,a,b) for(register int i=a;i<=b;++i)
#define f_(i,a,b) for(register int i=a;i>=b;--i)
#define ll long long
#define chu printf
#define rint register int
#define ull unsigned long long
inline ll re()
{
ll x=0,h=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')h=-1;ch=getchar();}
while(ch<='9'&&ch>='0'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*h;
}
#define int ll
const int N=1e6+100;
bool vis[N];
int prime[N],tot,cnt;
ll p[67],p2[67];//线性基
int T;
inline void Ins(ll x)
{
f_(i,62,0)
{
if(!(x&(1ll<<i)))continue;
if(!p[i]){p[i]=x;++cnt;return;}
x^=p[i];
}
}
inline void Divid(ll l,ll r,int len)
{
if(len>=17)
{
_f(i,1,len)Ins(1ll<<i);return;
}
//chu("divid;%lld--%lld:%d\n",l,r,len);
ll bj=sqrt(r);
fill(vis,vis+(r-l)+100,0);//查找在1~bj之间的质数,筛去区间内因子
for(rint i=1;i<=tot&&(ll)prime[i]<=bj;++i)
{
ll bas=l/prime[i];
if(bas==1||bas*prime[i]<l)++bas;
for(ll j=bas;j*prime[i]<=r;++j)//循环是这个质数的多少倍
vis[j*prime[i]-l+1]=1;//可以被表示,不是质数
}
for(ll i=l;i<=r;++i)
{
//chu("vis[%lld]:%d\n",i,vis[i-l+1]);
if(!vis[i-l+1])Ins(i);
}
}
signed main()
{
//freopen("sample-01.in","r",stdin);
//freopen("1.out","w",stdout);
T=re();
p2[0]=1;
_f(i,1,62)p2[i]=p2[i-1]*2ll;
_f(i,2,1000000)
{
if(!vis[i])prime[++tot]=i;
_f(j,1,tot)
{
if(prime[j]*i>1000000)break;
int bs=prime[j]*i;
vis[bs]=1;
if(i%prime[j]==0)break;
}
}
memset(vis,0,sizeof(vis));
while(T--)
{
ll let=re(),ret=re();cnt=0;
memset(p,0,sizeof(p));
ll now=let;//ret++;
_f(i,0,62)
{
if((now&(1ll<<i))&&now+(1ll<<i)-1<=ret)Divid(now,now+(1ll<<i)-1,i),now+=(1ll<<i);
}
// chu("now:%lld\n",now);
f_(i,62,0)
{
if(now+(1ll<<i)-1<=ret)Divid(now,now+(1ll<<i)-1,i),now+=(1ll<<i);
}
chu("%lld\n",p2[cnt]);
}
return 0;
}
/*
3
2 10
999999940 1000000000
2 1000000000000
1
999999940 1000000000
1
205286014976 205302792191
*/
9.26 CSP 模拟 45 联测 7
T1 难
很简单的一道题,基本所有人都场切了。
很明显,如果不排除掉重复的字符串,那么答案就是 (size1+1)×(size2+1)(size1+1)×(size2+1)。
考虑什么时候它会重。
当 s1s1 和 s2s2 有重复字符时。那么我们在那个位置上任选一个就行,比如:
s1:∗∗∗ a∗∗∗s1:∗∗∗ a∗∗∗
s2:∗∗∗ a∗∗∗s2:∗∗∗ a∗∗∗
∗∗ 代表其他任意字符。
那么我们可以选 ∗∗∗a#a∗∗∗∗∗∗a#a∗∗∗ 或 ∗∗∗a#∗∗∗∗∗∗a#∗∗∗ 或 ∗∗∗#a∗∗∗∗∗∗#a∗∗∗ 或 ∗∗∗#∗∗∗∗∗∗#∗∗∗
## 没有什么实际意义,只是作为第一个字符串和第二个字符串的分割。
我们发现,中间两个的实质是一样的。
所以有了一个结论,某一字符在 s1s1 出现的次数 ×× 在 s2s2 出现的次数,即为重复算的字符。
所以我们开两个桶维护一下每个字符在两个字符串中出现的次数,最后再从 aa 到 zz 遍历一遍就好了。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
int ans, tong1[27], tong2[27];
string s1, s2;
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> s1 >> s2;
ans = (s1.size() + 1) * (s2.size() + 1);
for(int i = 0; i < s1.size(); ++ i)
++ tong1[s1[i] - 'a'];
for(int i = 0; i < s2.size(); ++ i)
++ tong2[s2[i] - 'a'];
for(int i = 0; i <= 25; ++ i)
ans -= tong1[i] * tong2[i];
cout << ans << endl;
}
T2 点
赛时出了正解思路,但是没有调处来,,,
写的非常丑。
思路和题解大体一样,细节上不太一样,毕竟我是赛时思路。
设 fi,j(j∈{0,1})fi,j(j∈{0,1}) 代表以 ii 为子树的最大贡献,j=1j=1 代表边 (fai,i)(fai,i) 的权值被强制赋成了 aiai,而 j=0j=0 表示没有任何强制。
考虑怎么转移。
fi,0fi,0 很好转移,对于每一个 ii,我们都需要将与它相连的边按某一关键字排序,然后贪心的去取。
考虑取什么关键字。
设 vv 为 ii 的儿子。我们假设所有的都取 fv,0fv,0,即 fi,0=∑v∈sonifv,0fi,0=∑v∈sonifv,0。
但是这并不现实。
因为需要有 kk 个边(k=⌊outi2⌋+1k=⌊outi2⌋+1)小于等于 aiai。
所以有一些的贡献需要变为 fv,1fv,1。
关键字就是原来的贡献与新的贡献的差。
代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 1000005
#define mod 1000000007
#define P pair<int, int>
#define MP make_pair
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, m, a[M], f[M][2], k, out[M];
int head[M], to[M], nex[M], from[M], tot, lim[M];
inline void add_edge(int u, int v) {
nex[++ tot] = head[u];
head[u] = tot;
from[tot] = u;
to[tot] = v;
lim[tot] = m;
}
void dfs(int u, int fa, int x) {
pair<P, P> pa[out[u] + 1];
int len = 0;
if(x)
pa[++ len] = MP(MP(m - a[u], a[u]), MP(m, x));
for(int i = head[u]; i; i = nex[i]) {
int v = to[i];
if(v == fa)
continue;
dfs(v, u, i);
pa[++ len] = MP(MP(max(f[v][0], f[v][1]) - max((f[v][1] - max(0ll, lim[i] - a[u])), max(f[v][0], f[v][1]) - (m - a[u])), max((f[v][1] - max(0ll, lim[i] - a[u])), f[v][0] - (m - a[u]))), MP(max(f[v][0], f[v][1]), i));
// if(f[v][0] - (m - a[u]) > f[v][1] - max(0ll, lim[i] - a[u]))
// pa[++ len] = MP(MP(m, f[v][0] - (m - a[u])), MP(max(f[v][0], f[v][1]), i));
// else
// pa[++ len] = MP(MP(lim[i], f[v][1] - max(0ll, lim[i] - a[u])), MP(max(f[v][0], f[v][1]), i));
}
stable_sort(pa + 1, pa + 1 + len);
k = floor(len * 1.00 / 2) + 1;
bool op = 0;
for(int i = 1; i <= k; ++ i) {
f[u][0] += pa[i].first.second;
#ifdef ABCDEFG
cout << "???" << i << " " << pa[i].second.second << " " << from[pa[i].second.second] << " " << to[pa[i].second.second] << " " << pa[i].second.first << " " << pa[i].first.second << endl;
#endif
if(pa[i].second.second == x)
op = 1;
if(i < k)
f[u][1] += pa[i].first.second;
else {
if(pa[i].second.second != x)
f[u][1] += pa[i].second.first;
else
f[u][1] += pa[i].first.second;
}
}
for(int i = k + 1; i <= len; ++ i) {
#ifdef ABCDEFG
cout << "???" << i << " " << pa[i].second.second << " " << from[pa[i].second.second] << " " << to[pa[i].second.second] << " " << pa[i].second.first << " " << pa[i].first.second << endl;
#endif
f[u][0] += pa[i].second.first;
if(pa[i].second.second != x)
f[u][1] += pa[i].second.first;
else
f[u][1] += pa[i].first.second;
}
if(op) {
f[u][1] = f[u][0];
f[u][0] = -1;
}
lim[x] = a[u];
#ifdef ABCDEFG
cout << u << " " << f[u][0] << " " << f[u][1] << " " << lim[x] << " " << endl;
#endif
}
signed main() {
n = read();
m = read();
for(int i = 1; i <= n; ++ i)
a[i] = read();
for(int i = 1; i < n; ++ i) {
int u = read(), v = read();
add_edge(u, v);
add_edge(v, u);
++ out[u];
++ out[v];
}
dfs(1, 0, 0);
write(f[1][0]);
}
/*
5 10
8 9 7 6 2
2 1
3 1
4 3
5 1
24
*/
T3 不
题目大意
给定一个长度为 nn 的序列 ,你需要从中选出一个元素个数不少于 ⌈n2⌉⌈n2⌉ 的子序列,使得这个子序列中所有元素的 gcdgcd 最大。
分析
数据范围吓人。
106106,但是根本想不到什么 O(nlogn)O(nlogn) 或 O(n)O(n) 的算法。然后就开始想其他技巧。
刚开始想的是什么 gcdgcd 的性质,但是显然没有什么结果(我赛时就想到这里,然后寄了)。
注意到子序列元素个数比较大,可能比较容易从这入手,所以我们进一步从 ⌈n2⌉⌈n2⌉ 这里分析。
题解
既然我们选至少一半,那么就意味着每一个数都有 1212 的几率被选进最终答案的子集。我们可以考虑随机枚举几个数,假设我们枚举了 mm 个数,那么至少有一个数在答案的子集里的概率为 1−12m1−12m。这个概率其实蛮大的。
然后我们去想对于每一个枚举的数,我们怎么算可能的答案。
如果这个数在最终的子集里,那么意味着最终的 gcdgcd 一定是这个数的一个因数。所以我们考虑枚举这个数的所有因数,然后算每个因数在原序列中的出现位置,最后从大到小找到第一个出现次数大于等于 ⌈n2⌉⌈n2⌉ 的数,并与我们最终的 ansans 取 maxmax。
实现方面的话,记 cnticnti 表示第 ii 个因数在原序列中出现的次数。我们先对所有因数排序,然后从 11 到 nn 枚举原序列的数,与我们当前随机出来的数求 gcdgcd,求出来以后在存因数的那个数组里 lower_boundlower_bound 找(二分也行),使其 cnt+1cnt+1。但是这样明显是不对的,因为如果两个数的 gcdgcd 为 33,那么不仅 33 的出现次数加一,6,96,9 这一类数的出现次数也会多一次。所以我们最后求完 cntcnt 后再枚举一遍因数那个数组,然后对于每个因数 ii,再去枚举一遍小于它的因数 jj,如果 i%j=0i%j=0,那么令 cnti+=cntjcnti+=cntj,这才是最终的 cntcnt。
总时间复杂度 O(nlogd+d2)O(nlogd+d2) 差不多。dd 代表因数个数,其实蛮小的,对于 10121012 以内的数,最多的因数个数也才 67206720。mm 就是我代码里的 TT,我取的 1010,一发 AC 了,3.5s3.5s,没什么问题。
代码
如果你能看懂我用的随机化更好,看不懂就用普通的 rand()rand() 函数就行。
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define M 1000005
#define mod 1000000007
#define time() chrono::system_clock::now().time_since_epoch().count()
using namespace std;
inline int read() {
int x = 0, s = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-')
s = -s;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * s;
}
void write(int x) {
if(x < 0) {
x = ~(x - 1);
putchar('-');
}
if(x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
int n, a[M], ans, d[M >> 2], cnt[M >> 2];
signed main() {
n = read();
for(int i = 1; i <= n; ++ i)
a[i] = read();
int T = 10;
default_random_engine e;
uniform_int_distribution<int> Z(1, n);
srand(time());
e.seed(rand());
for(int t = 1; t <= T; ++ t) {
int r = Z(e);
int len = 0;
int x = a[r];
int sq = sqrt(x);
for(int i = 1; i <= sq; ++ i)
if(x % i == 0)
d[++ len] = i, cnt[len] = 0, d[++ len] = x / i, cnt[len] = 0;
if(sq * sq == x)
-- len;
stable_sort(d + 1, d + 1 + len);
for(int i = 1; i <= n; ++ i)
++ cnt[lower_bound(d + 1, d + 1 + len, __gcd(x, a[i])) - d];
for(int i = 1; i <= len; ++ i)
for(int j = i + 1; j <= len; ++ j)
if(d[j] % d[i] == 0)
cnt[i] += cnt[j];
for(int i = len; i >= 1; -- i) {
if(cnt[i] * 2 >= n) {
ans = max(ans, d[i]);
break;
}
}
}
write(ans);
}
/*
10
1 2 3 4 5 6 7 8 9 10
*/
T4 多
不会,没改,没看明白题解。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现