A. 星际穿越
感觉也很神。。
先将询问拆成前缀和的形式,我们要对于一组询问 $(l,r)$ 求出 $[l,r]$ 内所有点到 $r$ 的最短距离的和。
考虑 $r$ 往左走的最短路径是怎么走的:
引理一:往右走之前一定不会往左走
反证法。如果先往左走在往右走,那么这个右边的点一定能 cover 到起点,所以可以第一步就先往右走,答案不会变劣。
引理二:不会存在连续的两次往右走
反证法。如果存在两次连续的往右走,那么之后一定有一次往左走,并且跨越起点的,我们可以一开始就走到这个点,答案不会变劣。
所以我们的路径只有可能是先往右最多走一次,再往左走。
我们观察一下一个点 $v$ 到达左边的点 $x$ 的最短距离的分布是怎么样的:
如果 $dis(v,x)=1$,那么有 $l_v \leq x$。
如果 $dis(v,x) \leq 2$,那么我们就会先走一步,用这个点中转,也就是问在区间 $[l_v,n]$ 内是否存在点 $y$,使得 $dis(x,y) \leq 1$。注意这里这个区间包含了一些根本不跨过 $v$ 的点,但这是显然不影响答案的。
归纳可以得到:如果 $dis(v,x) \leq k$,当且仅当存在一个点 $y \in [l_v,n]$,满足 $dis(x,y) \leq k-1$。
所以一个暴力的想法是每次先特判了距离为 $1$ 的点,然后每次求出最往左的距离为 $2$ 的点跳过去,以此类推。。
对于跳这种东西,我们肯定想到要用倍增优化。我们想找到 $\leq k$ 的最往左的点,就要找到 $dis(x,y) \leq k-1$ 的最往左的点。。。所以我们定义在找 $dis(v,x) \leq k$ 的一次跳跃是从 $x$ 点找到最小的一个点 $z$,满足 $\exists y \in [l_v,n],dis(z,y) \leq k-1$,所以设 $f_{i,j}$ 表示从 $i$ 号点,跳了 $2^j$ 次(也就是处理了距离为 $[1,2^j+1]$ 的点),为了统计答案,设 $g_{i,j}$ 表示从 $i$ 号点跳了 $2^j$ 的所有答案,初始化 $f_{i,0} = \min_{j \geq i}\{l_j\}$,$f_{1,0} = 0,g_{i,0} = i-f_{i,0}$。转移大概写一下 :
$$ \begin{aligned} f_{i,j} &= f_{f_{i,j-1},j-1}\\ g_{i,j} &= g_{i,j-1}+g_{f_{i,j-1},j-1}+2^{j-1}(f_{i,j-1}-f_{i,j}) \end{aligned} $$
查询的时候跳一下,记一下已经跳了多少长度即可。最后有一些零散的边界要判掉。
#include <bits/stdc++.h>
#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl
const int MAXN = 3e5 + 5;
const int MAXM = 18;
int n,l[MAXN];
// 处理[i,n]跳2^j步的最远位置 和这一块的答案
int f[MAXN][MAXM+1];
LL g[MAXN][MAXM+1];
int pw[MAXM+1];
inline LL calc(int l,int p){
if(l > p) return 0;
if(l >= ::l[p]) return p-l;
LL res = p-::l[p],now = 1;p = ::l[p];
ROF(i,MAXM,0){
if(f[p][i] < l) continue;
res += g[p][i] + now*(p-f[p][i]);
now += (1<<i);
p = f[p][i];
}
if(l <= p) res += (now+1)*(p-l);
return res;
}
inline LL gcd(LL x,LL y){
return !y ? x : gcd(y,x%y);
}
int main(){
pw[0] = 1;FOR(i,1,MAXM) pw[i] = pw[i-1]<<1;
scanf("%d",&n);
FOR(i,2,n) scanf("%d",l+i),f[i][0] = l[i];
ROF(i,n-1,2) f[i][0] = std::min(f[i][0],f[i+1][0]),g[i][0] = i-f[i][0];
f[1][0] = 0;
FOR(j,1,MAXM){
FOR(i,1,n){
f[i][j] = f[f[i][j-1]][j-1];
g[i][j] = g[i][j-1]+g[f[i][j-1]][j-1]+(f[i][j-1]-f[i][j])*pw[j-1];
}
}
int q;scanf("%d",&q);
// DEBUG(calc(3,6));
// exit(0);
while(q--){
int l,r,x;scanf("%d%d%d",&l,&r,&x);
LL res = calc(l,x)-calc(r+1,x);
LL g = gcd(r-l+1,res);
printf("%lld/%lld\n",res/g,(r-l+1)/g);
}
return 0;
}
B. Minimax
设 $f_{v,i}$ 表示 $v$ 号点权值为 $i$ 的概率,考虑如何合并两个子树 $x,y$:
$$ f_{v,i} = f_{x,i}(p_v\sum_{j < i} f_{y,j}+(1-p_v)\sum_{j > i} f_{y,j}) + f_{y,i}(p_v\sum_{j < i} f_{x,j}+(1-p_v)\sum_{j > i} f_{x,j}) $$
这种 dp 维护可以搞个线段树合并,每次在区间 $[l,r]$ 的时候考虑同时维护一个对 $x,y$ 分别应该打什么标记,类似cdq分治那样处理左边对右边的贡献,和右边对左边的贡献即可。
#include <bits/stdc++.h>
#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl
const int MAXN = 3e5 + 5;
const int ha = 998244353;
const int inv = 796898467;
int n;
std::vector<int> G[MAXN];
int a[MAXN];
int rt[MAXN],sm[MAXN*25],tag[MAXN*25],lc[MAXN*25],rc[MAXN*25],tot;
inline void add(int &x,int y){
x += y-ha;x += x>>31&ha;
}
inline void cover(int x,int d){
// add(tag[x],d);
tag[x] = 1ll*tag[x]*d%ha;
sm[x] = 1ll*sm[x]*d%ha;
}
inline void pushdown(int x){
if(tag[x] != 1){
if(lc[x]) cover(lc[x],tag[x]);
if(rc[x]) cover(rc[x],tag[x]);
tag[x] = 1;
}
}
int p,M;
inline void update(int &x,int l,int r,int p){
x = ++tot;sm[x] = tag[x] = 1;
if(l == r) return;
int mid = (l + r) >> 1;
if(p <= mid) update(lc[x],l,mid,p);
else update(rc[x],mid+1,r,p);
}
inline int merge(int x,int y,int dx,int dy){
if(!x && !y) return 0;
if(!x || !y){
cover(x|y,x?dx:dy);
return x|y;
}
pushdown(x);pushdown(y);
int lsx = sm[lc[x]],rsx = sm[rc[x]],lsy = sm[lc[y]],rsy = sm[rc[y]];
lc[x] = merge(lc[x],lc[y],(dx+1ll*(1+ha-p)%ha*rsy%ha)%ha,(dy+1ll*(1+ha-p)%ha*rsx%ha)%ha);
rc[x] = merge(rc[x],rc[y],(dx+1ll*p*lsy%ha)%ha,(dy+1ll*p*lsx%ha)%ha);
sm[x] = (sm[lc[x]]+sm[rc[x]])%ha;
return x;
}
inline int query(int x,int l,int r,int p){
if(l == r) return sm[x];
int mid = (l + r) >> 1;pushdown(x);
if(p <= mid) return query(lc[x],l,mid,p);
return query(rc[x],mid+1,r,p);
}
inline void dfs(int v){
if(G[v].empty()){
update(rt[v],1,M,a[v]);
return;
}
for(auto x:G[v]) dfs(x);
if(G[v].size() == 1){
rt[v] = rt[G[v][0]];
return;
}
p = a[v];
rt[v] = merge(rt[G[v][0]],rt[G[v][1]],0,0);
}
int main(){
scanf("%d",&n);
FOR(i,1,n){
int f;scanf("%d",&f);
if(f) G[f].pb(i);
}
std::vector<int> S;
FOR(i,1,n){
scanf("%d",a+i);
if(G[i].empty()) S.pb(a[i]);
else a[i] = 1ll*a[i]*inv%ha;
}
std::sort(all(S));M = S.size();
FOR(i,1,n) if(G[i].empty()) a[i] = std::lower_bound(all(S),a[i])-S.begin()+1;
dfs(1);
int ans = 0;
FOR(i,1,M){
int t = query(1,1,M,i);
// DEBUG(t);
add(ans,1ll*i*S[i-1]%ha*t%ha*t%ha);
}
printf("%d\n",ans);
return 0;
}
C. 随机算法
考虑给定最终选出的一个独立集序列 $p_1,p_2,\ldots,p_m$,如何求有多大的概率生成的排列能得到这个序列。
首先第一个数一定是 $p_1$,接下来和 $p_1$ 相连的点就可以乱放了,在剩下的点中第一个要放的一定是 $p_2$,然后和 $p_2$ 相连,还没有被放完的点也可以乱放了。所以我们处理出 $sz_S$ 表示集合 $S$ 放完之后,有多少个点可以乱放(包含集合 $S$ 内的点),并且这个只对独立集有定义。
所以概率就是:
$$ \prod_{i=1}^m \frac{1}{n-sz_{\cup_{j=1}^{i-1}\{p_j\}}} $$
我们考虑对这个dp:设 $f_S$ 表示最终独立集序列为 $S$ ,所有 $|S|!$ 种方案的概率的和,答案显然就是每个最大独立集的 $f_S$。转移只需要枚举一个点,如果可以加入这个独立集就把对应的概率乘上就行了。
#include <bits/stdc++.h>
#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl
const int MAXN = 20;
const int ha = 998244353;
#define lowbit(x) ((x)&(-(x)))
int n,m,e[MAXN];
int ee[(1<<MAXN)+3],sz[(1<<MAXN)+3];// 选了独立集 S 后 位置可以自由选择的位置的数量
int f[(1<<MAXN)+3];// S 内 任意顺序排列概率的和
bool vis[(1<<MAXN)+3];
int inv[MAXN+1];
inline void add(int &x,int y){
x += y-ha;x += (x>>31&ha);
}
int main(){
inv[1] = 1;FOR(i,2,MAXN) inv[i] = (ha-1ll*(ha/i)*inv[ha%i]%ha)%ha;
scanf("%d%d",&n,&m);
FOR(i,1,m){
int u,v;scanf("%d%d",&u,&v);--u;--v;
e[u] |= (1<<v);e[v] |= (1<<u);
}
FOR(i,0,n-1) ee[1<<i] = e[i];
FOR(S,1,(1<<n)-1) ee[S] = ee[S-lowbit(S)]|ee[lowbit(S)];
int mx = 0;
FOR(S,0,(1<<n)-1) if(!(ee[S]&S)) vis[S] = 1,mx = std::max(mx,(int)__builtin_popcount(S));
FOR(S,0,(1<<n)-1) sz[S] = __builtin_popcount(ee[S]|S);
f[0] = 1;
FOR(S,0,(1<<n)-1){
if(!f[S]) continue;
if(!vis[S]) continue;
FOR(i,0,n-1){
if((S>>i)&1) continue;
if(!vis[S|(1<<i)]) continue;
add(f[S|(1<<i)],1ll*f[S]*inv[n-sz[S]]%ha);
}
}
int ans = 0;
FOR(S,0,(1<<n)-1) if(vis[S] && __builtin_popcount(S) == mx) add(ans,f[S]);
printf("%d\n",ans);
return 0;
}
One comment
膜拜队长!