2024.03 别急记录

1. IOI2018 - 狼人 / werewolf [省选/NOI-]

题意简述:多次询问求是否存在一条 st 的路径 a1,a2,...,ak 和路径上一个点 ai 使 a1,...,ai[L,n]ai,...,ak[1,R]

首先求出两棵 kruskal 重构树:

  • 第一棵树边权值设为 min(u,v),由大到小插入每一条边,树上点权设为子树内点权 min,则可通过倍增跳树的方式求得每个点经过 L 的点能到达的点集合(一个子树内所有叶子);
  • 第二棵树边权值设为 max(u,v),由小到大插入每一条边,树上点权设为子树内点权 max

因为一个子树内所有叶子是 dfn 序上一段连续区间,dfn 序又是一个排列,问题转化为了两个排列 a,b,求 a[l,r],b[p,q] 是否有交。

ai1 表示 ia 中的出现位置,若有交则等价于存在 x[p,q] 使 abx1[l,r]。设排列 ci=abi1,则等价于 c[p,q] 中存在值域 [l,r] 的数。

考虑使用主席树维护 c。每次询问查询 c[1,q],c[1,p1] 中值域 [l,r] 的数的个数。若二者不等则答案为 1,否则为 0

点击查看代码
const int N = 4e5 + 10;
int n, m, Q, fa[N], anti[N], c[N];
int ancmin[N][20], lenmin[N], ancmax[N][20], lenmax[N];
int dfnmin[N], dfnmax[N], mnmin[N], mxmin[N], mnmax[N], mxmax[N];
pair<int, int> emin[N], emax[N];
vector<int> gmin[N], gmax[N];
bool cmpmin(pair<int, int> x, pair<int, int> y){
return min(x.first, x.second) > min(y.first, y.second);
}
bool cmpmax(pair<int, int> x, pair<int, int> y){
return max(x.first, x.second) < max(y.first, y.second);
}
int gf(int x){
return x == fa[x] ? x : fa[x] = gf(fa[x]);
}
void dfsmin(int x){
if(x <= n){
++ *dfnmin;
dfnmin[*dfnmin] = x;
mnmin[x] = mxmin[x] = *dfnmin;
return;
}
mnmin[x] = 1e9;
for(int i : gmin[x]){
dfsmin(i);
mnmin[x] = min(mnmin[i], mnmin[x]);
mxmin[x] = max(mxmin[i], mxmin[x]);
}
}
void dfsmax(int x){
if(x <= n){
++ *dfnmax;
dfnmax[*dfnmax] = x;
mnmax[x] = mxmax[x] = *dfnmax;
return;
}
mnmax[x] = 1e9;
for(int i : gmax[x]){
dfsmax(i);
mnmax[x] = min(mnmax[i], mnmax[x]);
mxmax[x] = max(mxmax[i], mxmax[x]);
}
}
int jmpmin(int x, int val){
for(int i = 19; i >= 0; -- i){
if(lenmin[ancmin[x][i]] >= val){
x = ancmin[x][i];
}
}
return x;
}
int jmpmax(int x, int val){
for(int i = 19; i >= 0; -- i){
if(lenmax[ancmax[x][i]] <= val){
x = ancmax[x][i];
}
}
return x;
}
int rt[N], t[N*40], cnt, ls[N*40], rs[N*40];
int add(int p, int l, int r, int x, int v){
++ cnt;
t[cnt] = t[p];
ls[cnt] = ls[p];
rs[cnt] = rs[p];
p = cnt;
if(l == r){
t[p] += v;
} else {
int mid = l + r >> 1;
if(x <= mid){
ls[p] = add(ls[p], l, mid, x, v);
} else {
rs[p] = add(rs[p], mid+1, r, x, v);
}
t[p] = t[ls[p]] + t[rs[p]];
}
return p;
}
int qry(int p, int l, int r, int ql, int qr){
if(!p || qr < l || r < ql){
return 0;
} else if(ql <= l && r <= qr){
return t[p];
} else {
int mid = l + r >> 1;
return qry(ls[p], l, mid, ql, qr)
+ qry(rs[p], mid+1, r, ql, qr);
}
}
vector<int> check_validity(
int N,
vector<int> X,
vector<int> Y,
vector<int> S,
vector<int> E,
vector<int> L,
vector<int> R) {
vector<int> ans;
n = N;
m = X.size();
Q = S.size();
for(int i = 1; i <= m; ++ i){
emin[i].first = X[i-1];
emin[i].second = Y[i-1];
++ emin[i].first;
++ emin[i].second;
emax[i] = emin[i];
}
sort(emin + 1, emin + m + 1, cmpmin);
iota(fa + 1, fa + n + n + 1, 1);
iota(lenmin + 1, lenmin + n + 1, 1);
int cnt = n;
for(int i = 1; i <= m; ++ i){
int x = gf(emin[i].first), y = gf(emin[i].second);
if(x != y){
fa[x] = fa[y] = ++ cnt;
gmin[cnt].push_back(x);
gmin[cnt].push_back(y);
ancmin[x][0] = ancmin[y][0] = cnt;
lenmin[cnt] = min(lenmin[x], lenmin[y]);
}
}
dfsmin(cnt);
for(int i = cnt; i >= 1; -- i){
for(int j = 1; j < 20; ++ j){
ancmin[i][j] = ancmin[ancmin[i][j-1]][j-1];
}
}
sort(emax + 1, emax + m + 1, cmpmax);
iota(fa + 1, fa + n + n + 1, 1);
memset(lenmax, 0x3f, sizeof(lenmax));
iota(lenmax + 1, lenmax + n + 1, 1);
cnt = n;
for(int i = 1; i <= m; ++ i){
int x = gf(emax[i].first), y = gf(emax[i].second);
if(x != y){
fa[x] = fa[y] = ++ cnt;
gmax[cnt].push_back(x);
gmax[cnt].push_back(y);
ancmax[x][0] = ancmax[y][0] = cnt;
lenmax[cnt] = max(lenmax[x], lenmax[y]);
}
}
dfsmax(cnt);
for(int i = cnt; i >= 1; -- i){
for(int j = 1; j < 20; ++ j){
ancmax[i][j] = ancmax[ancmax[i][j-1]][j-1];
}
}
for(int i = 1; i <= n; ++ i){
anti[dfnmin[i]] = i;
}
for(int i = 1; i <= n; ++ i){
c[i] = anti[dfnmax[i]];
rt[i] = add(rt[i-1], 1, n, c[i], 1);
}
for(int i = 0; i < Q; ++ i){
int st = S[i], ed = E[i], le = L[i], ri = R[i];
++ st;
++ ed;
++ le;
++ ri;
int x = jmpmin(st, le), y = jmpmax(ed, ri);
int l = mnmin[x], r = mxmin[x], p = mnmax[y], q = mxmax[y];
int v = qry(rt[q], 1, n, l, r) - qry(rt[p-1], 1, n, l, r);
if(v){
ans.push_back(1);
} else {
ans.push_back(0);
}
}
return ans;
}

2. **AGC020C - Median Sum [*2259]

子集和中位数 O(nV) 做法!

mid=ai2,sum(S)=iSai,答案显然是 minS{sum(S)|sum(S)mid}。显然有一个 O(n2Vω)bitset 做法,不多讲。

由于所有的子集 S 数量过于多,考虑提取出其中的一部分:

若子集 S 使得不存在 i 使 sum(S)sum(S{i}) 同时 >mid 或同时 mid,称这样的 S平衡解

b=maxi{i|sum({1,2,...,i})mid},则易得 S={1,2,...,b} 为一个平衡解,设其为初始平衡解。设答案为 T,则易得 T 也为一个平衡解。

我们发现可以使用以下两种操作使得 S 变为任意一个平衡解,可以发现每次操作后 S 仍然为平衡解:

  • sum(S)mid,选择一个 iSS 变为 S{i}
  • sum(S)>mid,选择一个 iSS 变为 S{i}

ok(s,t,w)=1 当且仅当存在一个平衡解 S 使得 sum(S)=w1i<s,iS,t<in,iS,否则 ok(s,t,w)=0

可以发现对于 ss,ttok(s,t,w)ok(s,t,w)。所以我们设 f(t,w) 表示 maxs{s|ok(s,t,w)},若不存在这样的 sf(t,w)=0。那么存在平衡解 S 使得 sum(S)=w 当且仅当 f(n,w)0。最后计算答案的时候枚举 w 查看 f(n,w) 的值即可。

发现 w 这一维实际上有用的只有 O(V) 个位置。因为对于平衡解 Ssum(S)(midV,mid+V]。所以 f(t,w) 状态总数是 O(nV) 的。

考虑 dp:

初始化 f(b,sum({1,2,...,b}))=b+1。原因显然。

从小到大枚举 i[b+1,n],转移:

  • w(midV,mid+V],f(i,w)f(i1,w),表示不变。
  • w(midV,mid],f(i,w+ai)f(i1,w),表示插入一个 i
  • w(mid,mid+V],j<f(i,w),f(i,waj)j,表示删除一个 j

考虑第三种转移的复杂度:

发现当 j<f(i1,w) 时,j<f(i1,w)f(i1,waj),所以 f(i,waj) 在第一个转移中使用 f(i1,waj) 转移过来更优。所以 j[f(i1,w),f(i,w))。对于一个 w,所有 i 能对应到的 j 之和为 O(n) 的,所以总共的转移次数是 O(nV)。前两个转移的总转移次数显然是 O(nV)。所以总共的复杂度是 O(nV)

一个细节:第三个转移对于 w 的枚举应该从大到小,原因显然。

点击查看代码
const int N = 2e3 + 10;
int n, x, a[N];
int f[N*2], g[N*2];
void solve(){
read(n);
int sum = 0, all = 0;
for(int i = 1; i <= n; ++ i){
read(a[i]);
sum += a[i];
all += a[i];
x = max(x, a[i]);
}
sum /= 2;
int b = 0, w = 0;
while(w + a[b+1] <= sum){
++ b;
w += a[b];
}
f[w-sum+x] = b + 1;
for(int i = b + 1; i <= n; ++ i){
memcpy(g, f, sizeof(g));
for(int j = x; j; -- j){
f[j+a[i]] = max(f[j+a[i]], g[j]);
}
for(int j = x + x; j >= x + 1; -- j){
for(int k = f[j]-1; k >= max(1, g[j]); -- k){
f[j-a[k]] = max(f[j-a[k]], k);
}
}
}
int ans = 0;
for(int j = x; j; -- j){
if(f[j]){
ans = all - sum - j + x;
break;
}
}
println(ans);
}

3. 湖北省选模拟 2024 - 花神诞日 / sabzeruz [省选/NOI-]

显然有 abc,axorcmin{axorb,bxorc}。所以我们只用排序后从左往右考虑每个数,记录一下每道菜最后一个数是什么即可。

考虑设 fi,j 表示前 i 个数,第 i 个数被第一道菜选,第二道菜最后选的是第 j 个数的方案数;gi,j 表示前 i 个数,第 i 个数被第二道菜选,第一道菜最后选的是第 j 个数的方案数。

有四种转移:

  • [ai1xoraik1]fi1,jfi,j
  • [ai1xoraik2]gi1,jgi,j
  • [ajxoraik2]fi1,jgi,i1
  • [ajxoraik1]gi1,jfi,i1

那么我们使用 trie 树维护 (x,y)=(aj,fi,j) 的二元组,第三种操作实际上相当于对于 xxoraik2x 求和 y;第一种操作实际上相当于什么都不干或者全部置 0,可以在 trie 每个节点上维护一个 lazytag。第二、第四个操作同理。

最后,因为题目要求两个集合均非空。那么要减去所有数都归到一个集合的方案。

点击查看代码
const int N = 2e5 + 10;
const ll P = 1e9 + 7;
ll a[N], k1, k2;
int n, cnt = 2;
struct node{
int ch[2], tag;
ll sum;
} t[N*130];
void psd(int p){
if(t[p].tag){
if(t[p].ch[0]){
t[t[p].ch[0]].sum = 0;
t[t[p].ch[0]].tag = 1;
}
if(t[p].ch[1]){
t[t[p].ch[1]].sum = 0;
t[t[p].ch[1]].tag = 1;
}
t[p].tag = 0;
}
}
void ins(int p, ll v, ll x){
for(ll i = 62; i >= 0; -- i){
psd(p);
t[p].sum = (t[p].sum + x) % P;
if(!t[p].ch[(v>>i)&1]){
t[p].ch[(v>>i)&1] = ++ cnt;
}
p = t[p].ch[(v>>i)&1];
}
t[p].sum = (t[p].sum + x) % P;
}
ll qry(int p, ll v, ll w){
ll ans = 0;
for(ll i = 62; i >= 0; -- i){
psd(p);
if((w >> i) & 1){
if(t[p].ch[!((v>>i)&1)]){
p = t[p].ch[!((v>>i)&1)];
} else {
return ans;
}
} else {
ans += t[t[p].ch[!((v>>i)&1)]].sum;
if(t[p].ch[(v>>i)&1]){
p = t[p].ch[(v>>i)&1];
} else {
return ans;
}
}
}
ans = (ans + t[p].sum) % P;
return ans;
}
void fil(int p){
t[p].sum = 0;
t[p].tag = 1;
}
void solve(){
read(n, k1, k2);
for(int i = 1; i <= n; ++ i){
read(a[i]);
}
sort(a + 1, a + n + 1);
ins(1, (1ll<<61), 1);
ins(2, (1ll<<61), 1);
for(int i = 2; i <= n; ++ i){
ll sg = qry(1, a[i], k2);
ll sf = qry(2, a[i], k1);
if((a[i-1] ^ a[i]) < k1){
fil(1);
}
if((a[i-1] ^ a[i]) < k2){
fil(2);
}
ins(1, a[i-1], sf);
ins(2, a[i-1], sg);
}
int fa = 1, fb = 1;
for(int i = 2; i <= n; ++ i){
if((a[i-1] ^ a[i]) < k1){
fa = 0;
}
if((a[i-1] ^ a[i]) < k2){
fb = 0;
}
}
println((t[1].sum + t[2].sum - fa - fb + P) % P);
}

4. APIO2014 - 回文串 [省选/NOI-]

建出回文树,对每次增量添加一个字符后到达的点 cur,令 sumcur1。最后每个点代表的串的出现次数即为失配子树内 sum 和,拓扑序逆序(直接编号逆序即可)往根贡献即可。

点击查看代码
const int N = 3e5 + 10;
int n, tot = 1, len[N], sum[N], ch[N][26], fl[N], cur;
char s[N];
int gfl(int x, int i){
while(i - len[x] - 1 < 0 || s[i-len[x]-1] != s[i]){
x = fl[x];
}
return x;
}
void solve(){
scanf("%s", s + 1);
n = strlen(s + 1);
fl[0] = 1;
len[1] = -1;
for(int i = 1; i <= n; ++ i){
int pos = gfl(cur, i);
if(!ch[pos][s[i]-'a']){
fl[++tot] = ch[gfl(fl[pos], i)][s[i]-'a'];
ch[pos][s[i]-'a'] = tot;
len[tot] = len[pos] + 2;
}
cur = ch[pos][s[i]-'a'];
++ sum[cur];
}
for(int i = tot; i >= 0; -- i){
sum[fl[i]] += sum[i];
}
ll ans = 0;
for(int i = 0; i <= tot; ++ i){
ans = max(ans, (ll)len[i] * sum[i]);
}
printf("%lld\n", ans);
return;
}

5. APIO2023 - 赛博乐园 / cyberland [提高+/省选-]

考虑一层一层地跑。预处理出 1 不经过 H 能到达的点集,每次从这些点开始跑一遍 dij。然后称一次操作为:

  • 对于任意一条边 (u,v),若 arru=2,则使用 disu/2+w(u,v) 更新 disv
  • 从点集中点开始跑一遍 dij。

注意 inf 的取值。

点击查看代码
vector<pair<int, int>> g[N];
int a[N], ok[N], n, m, k, ed, vis[N];
double dis[N], nd[N];
void dfs(int x) {
ok[x] = 1;
for (auto i : g[x]) {
if (!ok[i.first] && i.first != ed) {
dfs(i.first);
}
}
}
void dij() {
priority_queue<pair<double, int>> q;
for (int i = 1; i <= n; ++ i) {
vis[i] = 0;
if (ok[i]) {
q.push(make_pair(-dis[i], i));
}
}
while (!q.empty()) {
int x = q.top().second;
q.pop();
if (vis[x]) {
continue;
}
vis[x] = 1;
for (auto i : g[x]) {
int y = i.first, z = i.second;
if (dis[y] > dis[x] + z) {
dis[y] = dis[x] + z;
q.push(make_pair(-dis[y], y));
}
}
}
}
void cpy() {
for (int i = 1; i <= n; ++ i) {
nd[i] = dis[i];
}
for (int i = 1; i <= n; ++ i) {
if (a[i] != 2 || dis[i] > 1e23) {
continue;
}
for (auto j : g[i]) {
int y = j.first, z = j.second;
dis[y] = min(dis[y], nd[i] / 2.0 + z);
}
}
}
double solve(
int nn,
int mm,
int kk,
int hh,
vector<int> x,
vector<int> y,
vector<int> c,
vector<int> arr) {
n = nn;
m = mm;
k = kk;
ed = hh + 1;
for (int i = 1; i <= n; ++ i) {
vector<pair<int, int>> ().swap(g[i]);
a[i] = arr[i - 1];
ok[i] = vis[i] = 0;
dis[i] = nd[i] = 1e24;
}
for (int i = 1; i <= m; ++ i) {
int X = x[i - 1] + 1, Y = y[i - 1] + 1, Z = c[i - 1];
if (X != ed)
g[X].emplace_back(Y, Z);
if (Y != ed)
g[Y].emplace_back(X, Z);
}
dfs(1);
for (int i = 1; i <= n; ++ i) {
if (ok[i] && (i == 1 || a[i] == 0)) {
dis[i] = 0;
}
}
k = min(k, 70);
dij();
while (k--) {
cpy();
dij();
}
double ans = dis[ed] > 1e23 ? (double) -1 : dis[ed];
for (int i = 1; i <= n; ++ i) {
vector<pair<int, int>> ().swap(g[i]);
a[i] = arr[i - 1];
ok[i] = vis[i] = 0;
dis[i] = nd[i] = 1e18;
}
return ans;
}

6. 省选联考 2024 - 魔法手杖 [NOI/NOI+/CTSC]

真不难吧这个题?为什么不会?

特判一下 bim 的情况,此时易得答案为 (minai)+2k1。否则至少有一个选择异或,此时答案一定 <2k

把所有 (a,b) 二元组插入一棵 trie 中。设最优答案为 now 以及对应的 x,考虑按位确定 now,x。对于遍历到的二叉树上节点 p,在更浅的分叉中钦定选择加法的最小值 sum,分四种情况:

  • now,x 这一位均为 1。则需要满足 p.ch1 中的所有点都选择加法,check 魔力值是否足够,更新 sum,递归到 p.ch0 中。
  • now 这一位为 1x 这一位为 0。与上一种类似。
  • now 这一位为 0x 这一位为 1。此时 p.ch0 中的点已经完全优于答案,不用管,递归到另一侧。
  • now,x 这一位均为 0。与上一种类似。

所以我们在递归求解的过程中需要记录 p,dep,lst,x,sum,now,分别表示节点编号、深度、还可用的魔力值大小、前面若干位最优的 x、已经钦定选择加法中最小的 a、前面若干位最优的答案。记录这些后就可以一遍深搜解决了。每个点最多遍历两次,所以复杂度 O(Tnk)

点击查看代码
//qoj8322
#include <bits/stdc++.h>
using namespace std; typedef long long ll;
void solve();int main(){ solve(); return 0; }
void read(__int128 &x){
// read a __int128 variable
char c; bool f = 0;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') { f = 1; c = getchar(); }
x = c - '0';
while ((c = getchar()) >= '0' && c <= '9') x = x * 10 + c - '0';
if (f) x = -x;
}
void write(__int128 x){
// print a __int128 variable
if (x < 0) { putchar('-'); x = -x; }
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
const int N = 1e5 + 10, K = 125;
int T, c, n, m, k, cnt;
__int128 a[N], ans;
ll b[N];
struct node{
int ch[2];
__int128 amin;
ll bsum;
} t[N*K];
__int128 zy(int x){
return (__int128) 1 << x;
}
__int128 fl(int x){
return zy(x) - 1;
}
void solve(int p, int dep, int lst, __int128 x, __int128 sum, __int128 now){
if(dep == -1){
ans = max(ans, now);
} else if(!p){
ans = max(ans, sum + (x | fl(dep+1)));
} else {
int lc = t[p].ch[0], rc = t[p].ch[1];
bool flg = 0;
if(t[lc].bsum <= lst && (x|fl(dep)) + min(sum,t[lc].amin) >= (now|zy(dep))){
solve(rc, dep-1, lst-t[lc].bsum, x, min(sum,t[lc].amin), now|zy(dep));
flg = 1;
}
if(t[rc].bsum <= lst && (x|fl(dep+1)) + min(sum,t[rc].amin) >= (now|zy(dep))){
solve(lc, dep-1, lst-t[rc].bsum, x|zy(dep), min(sum,t[rc].amin), now|zy(dep));
flg = 1;
}
if(!flg){
solve(lc, dep-1, lst, x, sum, now);
solve(rc, dep-1, lst, x|zy(dep), sum, now);
}
}
}
void solve(){
for(int i = 0; i < N*K; ++ i){
t[i].amin = zy(121);
}
scanf("%d%d", &c, &T);
while(T--){
scanf("%d%d%d", &n, &m, &k);
for(int i = 1; i <= n; ++ i){
read(a[i]);
}
for(int i = 1; i <= n; ++ i){
scanf("%lld", &b[i]);
}
cnt = 1;
for(int i = 1; i <= n; ++ i){
int p = 1;
t[p].bsum += b[i];
t[p].amin = min(t[p].amin, a[i]);
for(int j = k - 1; j >= 0; -- j){
int v = a[i] >> j & 1;
if(!t[p].ch[v]){
t[p].ch[v] = ++ cnt;
}
p = t[p].ch[v];
t[p].amin = min(t[p].amin, a[i]);
t[p].bsum += b[i];
}
}
if(t[1].bsum <= m){
ans = t[1].amin + zy(k) - 1;
} else {
solve(1, k-1, m, 0, zy(k), 0);
}
write(ans);
putchar('\n');
for(int i = 0; i <= cnt; ++ i){
t[i].ch[0] = t[i].ch[1] = t[i].bsum = 0;
t[i].amin = zy(121);
}
ans = 0;
}
}

7. 湖北省选模拟 2024 - 沉玉谷 / jade [省选/NOI-]

fi,j,k 表示 [i,j]k 次删空方案数,gi,j,k,l 表示 [i,j]k 次仅剩颜色 l 的方案数。

转移:

gl,r,x+y,ak=fl,k1,x(fk+1,r,y+gk+1,r,y,ak)(x+yx),即枚举最左边一个未删除的位置 k,两侧分别的操作数 x,y

fl,r,k=gl,r,k1,c,即枚举未删除的颜色 c

最后答案为 f1,n,i

初始化应设 fi+1,i,0=1(i[0,n])

点击查看代码
//P10202
#include <bits/stdc++.h>
using namespace std; typedef long long ll;
void solve();int main(){ solve(); return 0; }
const int N = 55;
const ll P = 1e9 + 7;
int n, a[N];
ll C[N][N], f[N][N][N], g[N][N][N][N];
void upd(ll &x, ll y){
x = (x + y) % P;
}
void solve(){
scanf("%d", &n);
C[0][0] = 1;
for(int i = 1; i <= n; ++ i){
scanf("%d", &a[i]);
C[i][0] = C[i][i] = 1;
for(int j = 1; j < i; ++ j){
C[i][j] = (C[i-1][j] + C[i-1][j-1]) % P;
}
f[i][i-1][0] = 1;
}
f[n+1][n][0] = 1;
for(int len = 1; len <= n; ++ len){
for(int l = 1; l + len - 1 <= n; ++ l){
int r = l + len - 1;
for(int k = l; k <= r; ++ k){
for(int x = 0; x <= k - l; ++ x){
for(int y = 0; y <= r - k; ++ y){
upd(g[l][r][x+y][a[k]],
f[l][k-1][x] *
(f[k+1][r][y] + g[k+1][r][y][a[k]]) % P *
C[x+y][x] % P);
}
}
}
for(int k = 1; k <= len; ++ k){
for(int c = 1; c <= n; ++ c){
upd(f[l][r][k], g[l][r][k-1][c]);
}
}
}
}
ll ans = 0;
for(int i = 1; i <= n; ++ i){
upd(ans, f[1][n][i]);
}
printf("%lld\n", ans);
}

8. ABC342F - Black Jack [*2141]

考虑设 gi 表示对方以 i 结尾的概率,fi 表示自己目前投出总和 i,按照最优方案最大获胜概率。

发现 g 很好求:gidgi+j[i<l][1jd]。稍微变形得 gi=gj[j<l][jid],可以前缀和优化。

pri=gj[lji],ls=gj[j>n],那么对于 i[l,n],自己在 i 处停止投的获胜概率是 pri1+ls

那么我们就有 f 的递推式:fi=max{fj[i+1ji+d]d,pri1+ls},表示继续投还是停止投,也可以前缀和优化。

点击查看代码
//AT_abc342_f
#include <bits/stdc++.h>
using namespace std; typedef long long ll;
void solve();int main(){ solve(); return 0; }
const int N = 4e5 + 10;
int n, l, d;
double f[N], g[N], sum[N], ls, pr[N];
void solve(){
scanf("%d%d%d", &n, &l, &d);
int m = n + l;
g[0] = sum[0] = 1;
for(int i = 1; i <= m; ++ i){
#define clc(x, y) (sum[y] - (x-1>=0 ? sum[x-1] : 0))
g[i] = clc(i-d, i-1) * 1.0 / d;
if(i < l){
sum[i] = sum[i-1] + g[i];
g[i] = 0;
} else {
sum[i] = sum[i-1];
pr[i] = pr[i-1] + g[i];
}
}
for(int i = 1; i <= m; ++ i){
if(i > n){
ls += g[i];
}
sum[i] = 0;
}
sum[0] = 0;
for(int i = n; i >= 0; -- i){
f[i] = max((sum[i+1] - sum[i+d+1]) / d, pr[i-1] + ls);
sum[i] = sum[i+1] + f[i];
}
printf("%.20lf\n", f[0]);
}

9. ABC342G - Retroactive Range Chmax [*2107]

做法解析:树套树。

对于线段树每个节点使用 std::multiset 维护可重集。

  • 对于操作 1,将 [l,r] 对应的若干个可重集中插入元素 x
  • 对于操作 2,将 [qi.l,qi.r] 对应的若干个可重集中删除元素 qi.x
  • 对于操作 3,输出 i 的祖先可重集中最大元素的最大值和 ai 中的较大值。
点击查看代码
//AT_abc342_g
#include <bits/stdc++.h>
using namespace std; typedef long long ll;
void solve();int main(){ solve(); return 0; }
const int N = 2e5 + 10;
int n, a[N], q, X[N], Y[N], Z[N];
multiset<int> t[N*4];
void add(int p, int l, int r, int ql, int qr, int x){
if(qr < l || r < ql){
return;
} else if(ql <= l && r <= qr){
t[p].insert(-x);
} else {
int mid = l + r >> 1;
add(p<<1, l, mid, ql, qr, x);
add(p<<1|1, mid+1, r, ql, qr, x);
}
}
void del(int p, int l, int r, int ql, int qr, int x){
if(qr < l || r < ql){
return;
} else if(ql <= l && r <= qr){
auto it = t[p].lower_bound(-x);
t[p].erase(it);
} else {
int mid = l + r >> 1;
del(p<<1, l, mid, ql, qr, x);
del(p<<1|1, mid+1, r, ql, qr, x);
}
}
int ans;
void qry(int p, int l, int r, int x){
ans = max(ans, t[p].empty() ? 0 : -*t[p].begin());
if(l != r){
int mid = l + r >> 1;
if(x <= mid){
qry(p<<1, l, mid, x);
} else {
qry(p<<1|1, mid+1, r, x);
}
}
}
void solve(){
scanf("%d", &n);
for(int i = 1; i <= n; ++ i){
scanf("%d", &a[i]);
}
scanf("%d", &q);
for(int i = 1; i <= q; ++ i){
int op, l, r, x;
scanf("%d%d", &op, &l);
if(op == 1){
scanf("%d%d", &r, &x);
tie(X[i], Y[i], Z[i]) = tie(l, r, x);
add(1, 1, n, l, r, x);
} else if(op == 2){
del(1, 1, n, X[l], Y[l], Z[l]);
} else {
ans = a[l];
qry(1, 1, n, l);
printf("%d\n", ans);
}
}
}

10. CCO2023 - Real Mountains [省选/NOI-]

bi=min(max1ji{aj},maxijn{aj}),显然有 b 是一组最小的可达的满足要求的方案。可以证明 b 是唯一的一组方案。所以我们的目标就变为求得最小代价使得 h 变为 b

容易猜测一个结论:每次选择 hi 最小且需要增加的位置,然后进行一次操作对其 +1。显然这个结论是正确的,证明可以考虑若存在 hj>hij 的操作在 i 前,两个交换一定不劣。那么如果有若干个 hi 并列最小值怎么办?

假设这个并列最小值为 x,有 k 个,我们要将这些全部变为 x+1。设最左侧的 xhl,最右侧的 xhr

最优操作方案是:对于左端点,我们找到二元组 (o,p),满足 ho>hl<hp,o<l<p,且不存在 hl<ho<ho,o<l,也不存在 hl<hp<hp,p>l。对于右端点,同理找到一个二元组 (q,r)

那么可以先进行一次 (o,l,p),一次 (l,r,q)(k2)(l,i,r) 完成目标。总代价为 ho+hr+hp+2k3+3(k1)x。若 hp>hq,还可以先操作右端点再操作左端点。故最优的总代价为 ho+min(hp,hq)+hr+2k3+3(k1)x

所以我们得到了一个暴力做法:遍历整个数组,提取出若干个 还需增加的并列最小值,找到两个二元组进行更新。复杂度爆炸。

考虑离散化。若出现在 h 中且大于最小值 x 的数为 y,则一定会使用相同的两个二元组更新相同数量、位置的 x 一路到 y。所以我们将三元组 (hi,i,1),(bi,i,1) 进行排序然后扫描线,可以将复杂度优化到 O(n2)

现在问题转化为了一个序列 h,两种操作:

  • 查询前缀/后缀中大于某值的最小值;
  • 一个抽象的修改操作。

发现很难处理修改。然而仔细分析可以发现修改是不需要的。因为根据上述算法,不可能存在两个位置 (i,j),最开始 hi<hj,在 中途的某个过程时既有 hj<bj 又有 hi>hj。所以只用维护查询操作,使用主席树即可。

点击查看代码
//qoj6626
#include <bits/stdc++.h>
using namespace std; typedef long long ll;
void solve();int main(){ solve(); return 0; }
const int N = 1e6 + 10;
const ll P = 1e6 + 3;
int n;
ll a[N], b[N];
tuple<ll, int, int> q[N*2];
ll ans;
ll Sum(ll x, ll y){
return (y * (y-1) / 2 % P - x * (x-1) / 2 % P + P) % P;
}
int t[N*100], ls[N*100], rs[N*100];
int rtl[N], rtr[N], cnt;
int add(int p, int l, int r, int x){
++ cnt;
tie(t[cnt], ls[cnt], rs[cnt]) = tie(t[p], ls[p], rs[p]);
p = cnt;
if(l == r){
++ t[p];
} else {
int mid = l + r >> 1;
if(x <= mid){
ls[p] = add(ls[p], l, mid, x);
} else {
rs[p] = add(rs[p], mid+1, r, x);
}
t[p] = t[ls[p]] + t[rs[p]];
}
return p;
}
int qry(int p, int l, int r, int ge){
if(t[p] == 0 || r <= ge){
return -1;
} else if(l == r){
return l;
} else {
int mid = l + r >> 1, tmp;
if((tmp = qry(ls[p], l, mid, ge)) > ge){
return tmp;
} else {
return qry(rs[p], mid+1, r, ge);
}
}
}
int qrl(int x, int mn){
return qry(rtl[x], 1, 1e9, mn);
}
int qrr(int x, int mn){
return qry(rtr[x], 1, 1e9, mn);
}
void solve(){
scanf("%d", &n);
for(int i = 1; i <= n; ++ i){
scanf("%lld", &a[i]);
q[i] = { a[i], i, -1 };
}
ll mx = 0;
rtl[0] = ++ cnt;
for(int i = 1; i <= n; ++ i){
mx = max(mx, a[i]);
rtl[i] = add(rtl[i-1], 1, 1e9, a[i]);
b[i] = mx;
}
mx = 0;
rtr[n+1] = ++ cnt;
for(int i = n; i >= 1; -- i){
mx = max(mx, a[i]);
b[i] = min(b[i], mx);
rtr[i] = add(rtr[i+1], 1, 1e9, a[i]);
q[i+n] = { b[i], i, 1 };
}
sort(q + 1, q + n + n + 1);
set<int> st;
for(int i = 1; i < n + n; ++ i){
if(get<2>(q[i]) == -1){
st.insert(get<1>(q[i]));
} else {
st.erase(get<1>(q[i]));
}
if(get<0>(q[i]) != get<0>(q[i+1]) && st.size()){
ll fr = get<0>(q[i]), to = get<0>(q[i+1]);
ll l = *st.begin(), r = *st.rbegin(), k = st.size();
ll oo = qrl(l, fr), pp = qrr(l, fr), qq = qrl(r, fr), rr = qrr(r, fr);
ll p = qrl(l, fr), q = qrr(r, fr);
if(k == 1){
ans += Sum(fr, to);
ans += (oo + pp) % P * (to - fr) % P;
} else {
ans += (oo + rr + min(pp, qq) + k + k - 3) % P * (to - fr) % P;
ans += 3 * (k - 1) % P * Sum(fr, to) % P;
}
ans %= P;
}
}
printf("%lld\n", ans);
}

11. NOI2010 - 海拔 [省选/NOI-]

首先易得每个点海拔为 01,且两部分分别连通。

然后就相当于平面图最小割,等于对偶图最短路即可。

注意建图时两个方向都要建图,每条边把它顺时针翻转一个九十度建边。

点击查看代码
const int N = 510;
int n, a[4][N][N];
vector<pair<int, int> > g[N*N];
int dis[N*N], vis[N*N];
#define cg(x, y) ((x-1)*n+y+1)
#define add(u, v, w) g[u].emplace_back(v, w)
void solve(){
scanf("%d", &n);
for(int i = 0; i < 4; i += 2){
for(int j = 0; j <= n; ++ j){
for(int k = 1; k <= n; ++ k){
scanf("%d", &a[i][j][k]);
}
}
for(int j = 1; j <= n; ++ j){
for(int k = 0; k <= n; ++ k){
scanf("%d", &a[i+1][j][k]);
}
}
}
int st = 0, ed = 1;
for(int i = 1; i <= n; ++ i){
for(int j = 1; j <= n; ++ j){
if(j == 1) add(st, cg(i, j), a[1][i][0]);
if(i == n) add(st, cg(i, j), a[0][n][j]);
if(j == n) add(cg(i, j), ed, a[1][i][n]);
if(i == 1) add(cg(i, j), ed, a[0][0][j]);
if(i > 1) add(cg(i, j), cg(i-1, j), a[0][i-1][j]);
if(i < n) add(cg(i, j), cg(i+1, j), a[2][i][j]);
if(j > 1) add(cg(i, j), cg(i, j-1), a[3][i][j-1]);
if(j < n) add(cg(i, j), cg(i, j+1), a[1][i][j]);
}
}
memset(dis, 0x3f, sizeof(dis));
dis[0] = 0;
priority_queue<pair<int, int> > q;
q.push(make_pair(0, 0));
while(!q.empty()){
int x = q.top().second;
q.pop();
if(vis[x]){
continue;
}
vis[x] = 1;
for(auto i : g[x]){
if(dis[i.first] > dis[x] + i.second){
dis[i.first] = dis[x] + i.second;
q.push(make_pair(-dis[i.first], i.first));
}
}
}
printf("%d\n", dis[1]);
}

12. NOI2010 - 超级钢琴 [省选/NOI-]

无脑两个 log 做法。

首先做个前缀和,定义一个状态 (i,k) 权值为 kthmaxj[i+L,min(i+R,n)]{sj}si,如果不存在这样的 kth 则为负无穷。

那么维护一个大根堆,首先将所有状态 (i,0) 插入。每次取队首 (i,j),添加答案,插入 (i,j+1) 即可。

查询 kthmax 可用主席树。

点击查看代码
const int N = 5e5 + 10;
int n, k, L, R, a[N], s[N], b[N], m;
int rt[N], t[N*30], ls[N*30], rs[N*30], cnt = 1;
int add(int p, int l, int r, int x){
++ cnt;
tie(t[cnt], ls[cnt], rs[cnt]) = tie(t[p], ls[p], rs[p]);
p = cnt;
if(l == r){
++ t[p];
} else {
int mid = l + r >> 1;
if(x <= mid){
ls[p] = add(ls[p], l, mid, x);
} else {
rs[p] = add(rs[p], mid+1, r, x);
}
t[p] = t[ls[p]] + t[rs[p]];
}
return p;
}
int qry(int p, int q, int l, int r, int x){
if(l == r){
return l;
} else {
int mid = l + r >> 1;
int k = t[rs[p]] - t[rs[q]];
if(k >= x){
return qry(rs[p], rs[q], mid+1, r, x);
} else {
return qry(ls[p], ls[q], l, mid, x-k);
}
}
}
int kmn(int l, int r, int k){
return qry(rt[r+1], rt[l], 1, m, k);
}
void solve(){
scanf("%d%d%d%d", &n, &k, &L, &R);
for(int i = 1; i <= n; ++ i){
scanf("%d", &a[i]);
s[i] = s[i-1] + a[i];
b[i] = s[i];
}
b[n+1] = 0;
sort(b + 1, b + n + 2);
m = unique(b + 1, b + n + 2) - b - 1;
priority_queue<tuple<int, int, int> > q;
for(int i = 0; i <= n; ++ i){
s[i] = lower_bound(b + 1, b + m + 1, s[i]) - b;
rt[i+1] = add(rt[i], 1, m, s[i]);
}
for(int i = 0; i <= n - L; ++ i){
q.push({ b[kmn(i+L, min(n, i+R), 1)] - b[s[i]], i, 1 });
}
ll ans = 0;
while(k--){
auto now = q.top();
q.pop();
ans += get<0>(now);
int x = get<1>(now), y = get<2>(now) + 1;
if(min(n, x+R) - (x+L) + 1 >= y){
q.push({ b[kmn(x+L, min(n, x+R), y)] - b[s[x]], x, y });
}
}
printf("%lld\n", ans);
}

13. CF838D - Airplane Arrangements [*2700]

首先考虑只从一个门进的情况。可以等价于:n+1 个位置排成一个环,m 个人,每个人选一个位置 i,若 i 已经被占用则继续选 i+1,若 n+1 已经被占用则继续选 1,最后找到一个未被占用的位置则占用。最后合法当且仅当 n+1 这个位置是空的。所有方案总数是 (n+1)m 个,而其中会有若干个合法的。可以发现每个最终状态出现概率是相同的,所以一个状态合法的概率即为 (n+1m)(nm)=n+1mn+1。答案是 (n+1)mn+1mn+1

而两个门其实不影响概率。所以答案是 (2n+2)mn+1mn+1

点击查看代码
const int N = 1e6 + 10;
typedef long long ll;
const ll P = 1e9 + 7;
int n, m;
ll qp(ll x, ll y){
ll ans = 1;
while(y){
if(y & 1){
ans = ans * x % P;
}
x = x * x % P;
y >>= 1;
}
return ans;
}
int main(){
cin >> n >> m;
cout << qp(n+n+2, m) * (n+1-m) % P * qp(n+1, P-2) % P;
return 0;
}

14. USACO19DEC - Tree Depth P [省选/NOI-]

首先,一个点的深度等于它的祖先个数 +1。所以我们只用枚举 (u,v),计算 vu 祖先的方案数即可。而 vu 祖先等价于 u,v 之间的数都 >av

考虑 dp 顺序:先 u,v 之间的点,再 u,再 v,最后其余所有点。发现这个 dp 实际上等价于 dp 所有点(可以看作一个分组背包)后再撤销掉一个 [0,uv] 的组。

为什么等价:首先 [u,v],(v,n] 肯定等价;[1,u) 可以看作翻转过后的若干项,也等价。

使用前缀和优化+撤销 dp 可以做到 O(n3) dp O(n2) 撤销。

之后,若 u<v,则 v 会和 [u,v) 构成逆序对,此时答案加上 fn1,k(vu);否则 v 不会和任何构成逆序对,答案加上 fn1,k。(n1 是因为撤销掉了一个组别)。

点击查看代码
const int N = 310;
int n, k;
ll P, f[2][N*N], s[N*N], ans[N];
#define ck(x) ((x) >= P ? (x) - P : (x))
int main(){
scanf("%d%d%lld", &n, &k, &P);
f[0][0] = 1;
for(int i = 1; i <= n; ++ i){
s[0] = f[i&1][0] = 1;
for(int j = 1; j <= n * n; ++ j){
f[i&1][j] = s[j] = ck(s[j-1] + f[(i-1)&1][j]);
if(j >= i){
f[i&1][j] = ck(f[i&1][j] - s[j-i] + P);
}
}
}
for(int i = 1; i <= n; ++ i){
ans[i] = f[n&1][k];
}
for(int t = 1; t < n; ++ t){
memset(s, 0, sizeof(s));
ll nw = P - 1;
s[0] = 1;
for(int i = 1; i <= n * n - t; ++ i){
if(i > t){
nw = ck(nw + s[i-t-1]);
}
s[i] = ck(f[n&1][i] + nw);
nw = ck(nw - s[i] + P);
}
for(int u = 1; u <= n - t; ++ u){
//v->u
if(t <= k){
ans[u] = ck(ans[u] + s[k-t]);
}
//u->v
ans[u+t] = ck(ans[u+t] + s[k]);
}
}
for(int i = 1; i <= n; ++ i){
printf("%lld ", ans[i]);
}
puts("");
}

15. CF1528F - AmShZ Farm [*3300]

题意中 a 的限制可以转化为:有 n 个人,每个人首先走到 ai,然后往后走直到第一个没有被占用的位置并占用。由上文 13. 得这种方案数为 (n+1)n1。我们在此时将 a 的值域扩展到 [1,n+1]。则对于任意的一个 a,其对应的 b 个数为 i=1n+1cntik。一个合法的 a 能够映射到 n 个不合法的 a,且这 n+1a 仅仅是平移可以得到。所以这 n+1a 对应的 b 个数都是相同的。所以我们仅用求出对于所有 (n+1)nb 个数总和除掉 n+1 即可。又因为对于任意的数 i,统计 abk=i 的贡献又都是相同的,所以 n+1i,两个 n+1 抵消,答案及为:

ans=i=0n(ni)iknni,即选择 i 个位置为钦定值,其它位置为剩余 n 种值。

推式子,参考 可得:

ans=i=0nni(n+1)ni{ki}

因为当 i>k{ki}=0,则:

ans=i=0min(n,k)ni(n+1)ni{ki}

前面两项可以线性预处理。最后一项需使用第二类斯特林数行。

点击查看代码
const int N = 4e5 + 10;
int n, k, m, tr[N];
typedef long long ll;
const ll P = 998244353, G = 3, iG = (P + 1) / G;
ll f[N], g[N], fac[N];
ll qp(ll x, ll y){
ll ans = 1;
while(y){
if(y & 1){
ans = ans * x % P;
}
x = x * x % P;
y >>= 1;
}
return ans;
}
void ntt(ll *f, int n, bool op){
for(int i = 0; i < n; ++ i){
if(i < tr[i]){
swap(f[i], f[tr[i]]);
}
}
for(int p = 2; p <= n; p <<= 1){
int len = p >> 1;
ll tg = qp(op ? G : iG, (P-1) / p);
for(int k = 0; k < n; k += p){
ll buf = 1;
for(int l = k; l < k + len; ++ l){
int tmp = buf * f[l+len] % P;
f[l+len] = (f[l] - tmp + P) % P;
f[l] = (f[l] + tmp) % P;
buf = buf * tg % P;
}
}
}
}
int main(){
cin >> n >> k;
int kk = k;
fac[0] = 1;
for(int i = 1; i <= k; ++ i){
fac[i] = fac[i-1] * i % P;
}
for(int i = 0; i <= k; ++ i){
f[i] = qp(i, k) * qp(fac[i], P-2) % P;
g[i] = qp(P-1, i) * qp(fac[i], P-2) % P;
}
for(m = k + k, k = 1; k <= m; k <<= 1);
for(int i = 0; i < k; ++ i){
tr[i] = (tr[i>>1] >> 1) | ((i & 1) ? k >> 1 : 0);
}
ntt(f, k, 1);
ntt(g, k, 1);
for(int i = 0; i < k; ++ i){
f[i] = f[i] * g[i] % P;
}
ntt(f, k, 0);
ll ans = 0, dm = 1, pw = qp(n+1, n), inv = qp(n+1, P-2), in = qp(k, P-2);
for(int i = 0; i <= min(n, kk); ++ i){
ans = (ans + dm * pw % P * f[i] % P * in % P) % P;
dm = dm * (n-i) % P;
pw = pw * inv % P;
}
cout << ans << '\n';
return 0;
}

16. BalkanOI2018 - Popa [省选/NOI-]

你怎么知道我不会笛卡尔树?

我们可以发现动态维护的右链上一定能找到一个目前点的因数,所以做法是对的。

点击查看代码
int query(int a, int b, int c, int d);
int solve(int N, int* Left, int* Right){
stack<int> st;
for(int i = 0; i < N; ++ i){
int le = -1;
while(!st.empty() && query(st.top(), i, i, i)){
le = st.top();
st.pop();
}
Left[i] = le;
Right[i] = -1;
if(!st.empty()){
Right[st.top()] = i;
}
st.push(i);
}
int le = -1;
while(!st.empty()){
le = st.top();
st.pop();
}
return le;
}

17. Ynoi ER 2024 - TEST_132 [省选/NOI-]

你可以根号分治的。

点击查看代码
const int N = 1e6 + 10;
typedef long long ll;
const ll P = 1e9 + 7, Q = 1e9 + 6;
int n, m, x[N], y[N], c[N];
vector<int> g[N], mx;
vector<pair<int, int> > f[N];
ll v[N], ans[N], pw[N];
ll qp(ll x, ll y){
ll ans = 1;
while(y){
if(y & 1){
ans = ans * x % P;
}
x = x * x % P;
y >>= 1;
}
return ans;
}
int main(){
scanf("%d%d", &n, &m);
int B = 1000000;
for(int i = 1; i <= n; ++ i){
scanf("%d%d%lld", &x[i], &y[i], &v[i]);
g[x[i]].push_back(i);
++ c[x[i]];
}
for(int i = 1; i <= n; ++ i){
pw[i] = 1;
if(c[i] > B){
mx.push_back(i);
}
}
for(int i = 1; i <= n; ++ i){
if(c[x[i]] > B){
f[y[i]].emplace_back(v[i], x[i]);
} else {
ans[y[i]] = (ans[y[i]] + v[i]) % P;
}
}
while(m--){
int op, p;
scanf("%d%d", &op, &p);
if(op == 1){
if(c[p] <= B){
for(int i : g[p]){
ans[y[i]] = (ans[y[i]] + P - v[i]) % P;
v[i] = v[i] * v[i] % P;
ans[y[i]] = (ans[y[i]] + v[i]) % P;
}
} else {
pw[p] = pw[p] * 2 % Q;
}
} else {
ll res = ans[p];
for(auto i : f[p]){
res = (res + qp(i.first, pw[i.second])) % P;
}
printf("%lld\n", res);
}
}
return 0;
}
posted @   KiharaTouma  阅读(16)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起