『模拟赛』NOIP2024加赛8
Rank
唐
A. flandre
签。
比较显然,由于 \(k\ge 0\),所以最终的序列一定为不降序列。首先将原序列升序排序,当任取一个子序列时,容易发现最后一个数越大一定越优,那么最后一个数取到最大值时,倒数第二个数一定越大越优,以此类推,最终取出的序列一定是原序列的一个后缀。我们倒序枚举逐个选择,当选了某个烟花答案变得不优时结束选择即可。复杂度瓶颈是排序的 \(\mathcal{O(n\log n)}\)。
说句闲话:四个核的虚拟机跑大样例要 1s 多,换成 DEV 或者本机 vscode 只用 0.3s。
点击查看代码
#include<bits/stdc++.h>
#define fo(x, y, z) for(int (x) = (y); (x) <= (z); (x)++)
#define fu(x, y, z) for(int (x) = (y); (x) >= (z); (x)--)
using namespace std;
typedef long long ll;
#define lx ll
inline lx qr()
{
char ch = getchar(); lx x = 0, f = 1;
for(; ch < '0' || ch > '9'; ch = getchar()) if(ch == '-') f = -1;
for(; ch >= '0' && ch <= '9'; ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ 48);
return x * f;
}
#undef lx
#define qr qr()
#define fi first
#define se second
#define pii pair<int, int>
#define P_B(x) push_back(x)
#define M_P(x, y) make_pair(x, y)
const int Ratio = 0;
const int N = 1e6 + 5;
int n, k;
int a[N], id[N];
ll ans;
int st[N], top;
namespace Wisadel
{
ll zc[N], zct;
inline void Wqw(ll x)
{
if(!x){putchar('0'); return ;}
while(x) zc[++zct] = x % 10, x /= 10;
while(zct) putchar(zc[zct--] + '0');
}
short main()
{
freopen("flandre.in", "r", stdin), freopen("flandre.out", "w", stdout);
n = qr, k = qr;
fo(i, 1, n) a[i] = qr, id[i] = i;
sort(id + 1, id + 1 + n, [](int A, int B){return a[A] > a[B];});
int tot = 0, nowtot = 0;
fo(i, 1, n)
{
if(i == 1) nowtot = 1;
else if(nowtot && a[id[i]] != a[st[top]]) tot += nowtot, nowtot = 1;
else if(nowtot && a[id[i]] == a[st[top]]) nowtot++;
ll zc = ans + 1ll * k * tot + a[id[i]];
if(zc >= ans) ans = zc, st[++top] = id[i];
else break;
}
Wqw(ans), putchar(' '), Wqw(top), puts("");
while(top) Wqw(st[top--]), putchar(' ');
puts("");
return Ratio;
}
}
signed main(){return Wisadel::main();}
B. meirin
这题没切纯唐了。
所求答案可以化成形如 \(\sum_{i=1}^n (x_1a_1+x_2a_2+\cdots +x_na_n)b_i\) 的形式。发现 \(a_i\) 是不变的,我们只要求出每个 \(b_i\) 前的系数,然后搬到线段树上就做完了。问题就在求这个系数上。赛时一直在拆分到每个 \(a_i\) 前的系数上,导致无论怎么优化都只能达到 \(\mathcal{O(n^2)}\) 求。
发现单点求是没有前途的,我们考虑对一个区间求。如下图,我们求点 \(i\) 的系数贡献,本质上是求所有包含点 \(i\) 的区间的 \(a_i\) 之和。
考虑枚举左侧的区间,那么其可以对应的是右侧所有区间,记录前缀和的前缀和,可以对每一个左侧区间 \(\mathcal{O(1)}\) 求出贡献。同理,对于每个右侧的区间,其可以对应的是左侧所有区间,记录后缀和的后缀和,可以 \(\mathcal{O(1)}\) 求出。那么对于每个点做一次求出系数预处理,复杂度就来到了 \(\mathcal{O(n)}\)。那么每次修改,只需区间和求出对应的系数和,就可以直接求出结果。时间复杂度是 \(\mathcal{O(n+q\log n)}\)。
注意预处理时边界的处理。
点击查看代码
#include<bits/stdc++.h>
#define fo(x, y, z) for(int (x) = (y); (x) <= (z); (x)++)
#define fu(x, y, z) for(int (x) = (y); (x) >= (z); (x)--)
using namespace std;
typedef long long ll;
#define lx ll
inline lx qr()
{
char ch = getchar(); lx x = 0, f = 1;
for(; ch < '0' || ch > '9'; ch = getchar()) if(ch == '-') f = -1;
for(; ch >= '0' && ch <= '9'; ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ 48);
return x * f;
}
#undef lx
#define qr qr()
#define fi first
#define se second
#define pii pair<int, int>
#define P_B(x) push_back(x)
#define M_P(x, y) make_pair(x, y)
const int Ratio = 0;
const int N = 5e5 + 5;
const int mod = 1e9 + 7;
int n, q;
ll a[N], b[N];
ll v[N << 2], zz[N];
ll s[N], ss[N], h[N], hh[N];
namespace Wisadel
{
#define ls (rt << 1)
#define rs (rt << 1 | 1)
#define mid ((l + r) >> 1)
inline void Wpushup(int rt){v[rt] = (v[ls] + v[rs]) % mod;}
inline void Wbuild(int rt, int l, int r)
{
if(l == r)
{
v[rt] = zz[l];
return ;
}
Wbuild(ls, l, mid), Wbuild(rs, mid + 1, r);
Wpushup(rt);
}
inline ll Wq(int rt, int l, int r, int x, int y)
{
if(x <= l && r <= y) return v[rt];
ll res = 0;
if(x <= mid) res = (res + Wq(ls, l, mid, x, y)) % mod;
if(y > mid) res = (res + Wq(rs, mid + 1, r, x, y)) % mod;
return res;
}
short main()
{
freopen("meirin.in", "r", stdin), freopen("meirin.out", "w", stdout);
n = qr, q = qr;
fo(i, 1, n) a[i] = qr;
fo(i, 1, n) b[i] = qr;
fo(i, 1, n) s[i] = (s[i - 1] + a[i]) % mod;
fu(i, n, 1) h[i] = (h[i + 1] + a[i]) % mod;
fo(i, 1, n) ss[i] = (ss[i - 1] + s[i]) % mod;
fu(i, n, 1) hh[i] = (hh[i + 1] + h[i]) % mod;
ll ans = 0;
fo(i, 1, n)
{
ll zc = ((ss[n] - ss[i - 1] + mod) % mod - s[i - 1] * (n - i + 1) % mod + mod) % mod * i % mod;
zc = (zc + ((hh[1] - hh[i] + mod) % mod - h[i] * (i - 1) % mod + mod) % mod * (n - i + 1) % mod) % mod;
zz[i] = zc;
ans = (ans + zz[i] * b[i] % mod) % mod;
}
Wbuild(1, 1, n);
fo(i, 1, q)
{
int l = qr, r = qr; ll k = (qr % mod + mod) % mod;
ll zc = Wq(1, 1, n, l, r) * k % mod;
ans = (ans + zc) % mod;
printf("%lld\n", ans);
}
return Ratio;
}
}
signed main(){return Wisadel::main();}
C. sakuya
比较诈骗,赛时题面把大家都吓住了。
考虑期望的本质是所有方案代价的平均数,我们对于每条边统计其贡献,求和之后除以方案数即可。对于每一条边,考虑统计其代价的充要条件是在其连接的两个连通块中,各存在一个特殊点且要求二者在序列中相邻。那么设其中一端的子树内有 \(s\) 个特殊点,则另一端有 \(m-s\) 个特殊点,可能的方案数共有 \(2s(m-s)\) 个(双向)。将其提出到序列中,它们可以在序列中的任意地方出现,故方案数乘上 \(m-1\),同时其他 \(m-2\) 个特殊点的顺序不做要求,故还要乘上 \((m-2)!\)。最后乘上其边权就得出了这条边的贡献。
考虑加入修改如何做。将上述边贡献中 \(2s(m-s)(m-1)(m-2)!\) 看做系数,统计每个点所连边的系数之和,容易发现给这个点所连边加边权等价于给系数之和乘上 \(k\) 加入答案,那么就做完了。直接 dfs 求出每个点子树内特殊点数量,然后处理每条边即可。时间复杂度 \(\mathcal{O(n+q)}\)。
点击查看代码
#include<bits/stdc++.h>
#define fo(x, y, z) for(int (x) = (y); (x) <= (z); (x)++)
#define fu(x, y, z) for(int (x) = (y); (x) >= (z); (x)--)
using namespace std;
typedef long long ll;
#define lx ll
inline lx qr()
{
char ch = getchar(); lx x = 0, f = 1;
for(; ch < '0' || ch > '9'; ch = getchar()) if(ch == '-') f = -1;
for(; ch >= '0' && ch <= '9'; ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ 48);
return x * f;
}
#undef lx
#define qr qr()
#define fi first
#define se second
#define pii pair<int, int>
#define P_B(x) push_back(x)
#define M_P(x, y) make_pair(x, y)
const int Ratio = 0;
const int N = 5e5 + 5;
const int mod = 998244353;
int n, m, q;
int b[N], siz[N];
int hh[N], to[N << 1], ne[N << 1], cnt = 1;
ll qzs[N << 1], f[N], ans, jc[N];
namespace Wisadel
{
inline ll Wqp(ll x, int y)
{
ll res = 1;
while(y){if(y & 1) res = res * x % mod; x = x * x % mod; y >>= 1;}
return res;
}
inline void Wadd(int u, int v, ll val)
{
to[++cnt] = v;
qzs[cnt] = val;
ne[cnt] = hh[u];
hh[u] = cnt;
}
inline void Wdfs(int u, int fa)
{
siz[u] = b[u];
for(int i = hh[u]; i != -1; i = ne[i])
{
int v = to[i];
if(v == fa) continue;
Wdfs(v, u);
siz[u] += siz[v];
ll zc = 2ll * siz[v] * (m - siz[v]) % mod * (m - 1) % mod * jc[m - 2] % mod;
f[u] = (f[u] + zc) % mod, f[v] = (f[v] + zc) % mod;
ans = (ans + zc * qzs[i] % mod) % mod;
}
}
short main()
{
freopen("sakuya.in", "r", stdin), freopen("sakuya.out", "w", stdout);
n = qr, m = qr;
jc[0] = 1; fo(i, 1, m) jc[i] = jc[i - 1] * i % mod;
memset(hh, -1, sizeof hh);
fo(i, 1, n - 1)
{
int a = qr, b = qr; ll c = qr;
Wadd(a, b, c), Wadd(b, a, c);
}
fo(i, 1, m) b[qr] = 1;
Wdfs(1, 0);
q = qr;
fo(i, 1, q)
{
int x = qr; ll k = qr;
ans = (ans + f[x] * k % mod) % mod;
printf("%lld\n", ans * Wqp(jc[m], mod - 2) % mod);
}
return Ratio;
}
}
signed main(){return Wisadel::main();}
D. 红楼 ~ Eastern Dream
容易发现模数和区间修改的段数有关,模数越大段数越小。首先考虑根号分治。对于小于 \(\sqrt{n}\) 的模数,我们记录每个模数对应的循环中所有位置的修改值并对其做前缀和处理,那么我们查询时只用遍历这 \(\sqrt{n}\) 个模数,分讨求出该区间新加的值之和即可。
对于大于 \(\sqrt{n}\) 的模数,容易想到线段树处理,可这样依然会对至多 \(\sqrt{n}\) 个区间做修改,修改复杂度是 \(\mathcal{O(n\log n\sqrt{n})}\) 的,查询是 \(\mathcal{O(n\log n)}\) 的。
发现修改和查询复杂度比例为 \(\sqrt{n}\),因此考虑根号平衡。直接对序列分块。发现我们要做到 \(\mathcal{O(1)}\) 查询,因此考虑差分实现,维护每一块的差分数组之和。画出图来发现每一块的贡献是一个矩形加上一个三角形的面积。矩形的一条边是询问区间长度,另一条边长度就是差分数组和。而三角形的形状是不变的,直接维护三角形面积即可。考虑怎么修改。对矩形边的修改是平凡的,对三角形的修改,考虑底变了,高不变,因此直接乘上高加入面积即可。之后就是平凡的分块操作,然后就做完了。整体复杂度 \(\mathcal{O(n\sqrt{n})}\)。
点击查看代码
#include<bits/stdc++.h>
#define fo(x, y, z) for(int (x) = (y); (x) <= (z); (x)++)
#define fu(x, y, z) for(int (x) = (y); (x) >= (z); (x)--)
using namespace std;
typedef long long ll;
#define lx ll
inline lx qr()
{
char ch = getchar(); lx x = 0, f = 1;
for(; ch < '0' || ch > '9'; ch = getchar()) if(ch == '-') f = -1;
for(; ch >= '0' && ch <= '9'; ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ 48);
return x * f;
}
#undef lx
#define qr qr()
#define fi first
#define se second
#define pii pair<int, int>
#define P_B(x) push_back(x)
#define M_P(x, y) make_pair(x, y)
const int Ratio = 0;
const int N = 2e5 + 5, NN = 450;
int n, m;
int a[N];
int bl[N], ed[N], sq;
ll s1[N], s2[N], lazy, f[NN][NN], c[N];
namespace Wisadel
{
inline void Wupd(int x, ll k)
{
int zc = bl[x];
c[x] += k, s1[zc] += k, s2[zc] += k * (ed[zc] - x + 1);
}
inline ll Wq(int l, int r)
{
ll res = 0;
fo(i, 1, bl[l] - 1) res += s1[i] * (r - l + 1);
fo(i, ed[bl[l] - 1] + 1, l - 1) res += c[i] * (r - l + 1);
if(bl[r] - bl[l] <= 1)
{
fo(i, l, r) res += c[i] * (r - i + 1);
}
else
{
fo(i, l, ed[bl[l]]) res += c[i] * (r - i + 1);
fo(i, bl[l] + 1, bl[r] - 1) res += s1[i] * (r - ed[i]) + s2[i];
fo(i, ed[bl[r] - 1] + 1, r) res += c[i] * (r - i + 1);
}
return res;
}
short main()
{
freopen("scarlet.in", "r", stdin), freopen("scarlet.out", "w", stdout);
n = qr, m = qr;
fo(i, 1, n) a[i] = qr, c[i] = a[i] - a[i - 1];
sq = sqrt(n);
fo(i, 1, sq) ed[i] = n / sq * i;
ed[sq] = n;
fo(i, 1, sq) fo(j, ed[i - 1] + 1, ed[i]) bl[j] = i, s1[i] += c[j], s2[i] += 1ll * c[j] * (ed[i] - j + 1);
fo(i, 1, m)
{
int op = qr, l = qr, r = qr; ll k;
if(op == 1)
{
k = qr;
if(l <= r + 1) lazy += k;
else if(l < sq)
{
ll zc = 0;
fo(j, 0, r) zc += k, f[l][j] += zc;
fo(j, r + 1, l - 1) f[l][j] += zc;
}
else
{
for(int j = 0; j < n; j += l)
{
Wupd(j + 1, k);
if(j + 1 + r + 1 <= n) Wupd(j + 2 + r, -k);
}
}
}
else
{
ll ans = lazy * (r - l + 1);
fo(j, 1, sq - 1)
{
int tim = ((r - 1) / j) - ((l + j - 1) / j);
if(tim > 0) ans += f[j][j - 1] * tim;
int L = (l - 1 + j) % j, R = (r - 1 + j) % j;
if((r - 1) / j == (l - 1) / j) ans -= f[j][j - 1];
ans += f[j][j - 1] - (L ? f[j][L - 1] : 0) + f[j][R];
}
ans += Wq(l, r);
printf("%lld\n", ans);
}
}
return Ratio;
}
}
signed main(){return Wisadel::main();}
末
赛时状态一般,T2 一直没往区间和上想属实是唐了。T3 被题面吓到没敢深究,T4 打的暴力喜提 70pts。
NOIP 这样的状态真就 G 了。考前打这样一场模拟赛也算是长教训。研究题不能只在一个方向上搞。点不行想线,线要再扩展到面。全面地研究每道题,才能得出真正优的解法。
以及细节处理上,T1 上来就打,越打越发现有些东西没处理,然后改来改去错失首 A。好在这只是签,码短好调,如果在一道码力题上犯这样的错,可能错失的就是 100pts 了。Think twice, code once.
还剩一场终结赛,全力以赴,打出气势。
完结撒花~
~