蜜月日记 Part10

· · 休闲·娱乐

前言

Day 100 最后冲刺!

Day 91

由于原本这一天的题过于简单了且没有技巧所以换掉。

神秘线代科技。

2025/06/30 [PA 2022] Drzewa rozpinające

题意

给定一个长为 n 的序列 a_1,\cdots,a_n。有 n 个点,第 i 个点和第 j 个点之间连了 \gcd(a_i,a_j) 条边,求这张图的生成树个数对 10^9+7 取模的值。

思路

设值域为 m

先回忆一下无向图矩阵树定理。设 D 为度数矩阵去掉最后一行最后一列,G 为邻接矩阵去掉最后一行最后一列,则生成树个数为 \det(D-G)

直接计算就 O(n^3) 了显然是跑不过的。我们需要用行列式的性质化简。

事实上,我们有:

【矩阵行列式引理】 对于 n\times n 的可逆矩阵 An\times m 的矩阵 U,V,有 \det(A+UV^{\top})=\det A\det(I+V^{\top}A^{-1}U)

如果 A 的行列式好算(比如对角矩阵),且 \det(I+V^{\top}A^{-1}U) 更好算(比如 m<<n 时)可以用矩阵行列式引理来算。

回到矩阵树定理。发现这个定理的形式就是给我们用这个的。我们要求 \det(D-G),其中 D 是对角矩阵行列式好算,只需要找到合适的 U,V 即可。

观察 G 的结构,我们有 G_{i,j}=\gcd(a_i,a_j)=\sum\limits_{d|a_i,d|a_j}\varphi(d)=\sum\limits_{d=1}^m[d|a_i]\varphi(d)[d|a_j]

于是构造 (n-1)\times m 的矩阵 U,V,其中 U_{i,d}=[d|a_i]\varphi(d),V_{i,d}=[d|a_j],则 G=UV^{\top}

那么 (I-V^{\top}D^{-1}U)_{i,j}=[i=j]-\varphi(j)\sum\limits_{t=1}^{n-1}[i|a_t][j|a_t]。这个只在 \operatorname{lcm}(i,j)\le m 的时候有值。

每次只消元有值的位置,复杂度能过。不能过把矩阵转置一下就过了。

代码

#include<bits/stdc++.h>
#define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i)
#define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i)
#define ll long long
#define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout)
using namespace std;
const int MAXN=5001,MOD=1e9+7;
inline ll qpow(ll b,int e){ll res=1;while(e)(e&1)&&(res=res*b%MOD),b=b*b%MOD,e>>=1;return res;}
int n,m,a[MAXN];
int vis[MAXN],phi[MAXN];
vector<int>pr;
inline void init(){
    phi[1]=1;
    F(i,2,m){
        if(!vis[i]) vis[i]=i,phi[i]=i-1,pr.emplace_back(i);
        for(int j:pr){
            int k=i*j;
            if(k>m) break;
            vis[k]=j;
            if(vis[i]==j){phi[k]=phi[i]*j;break;}
            else phi[k]=phi[i]*(j-1);
        }
    }
}
int D[MAXN],G[MAXN][MAXN];
inline ll det(){
    bool inv=0;
    ll res=1;
    F(i,1,m){
        int t=-1;
        F(j,i,m) if(G[j][i]){t=j;break;}
        if(t==-1) return 0;
        if(t!=i) swap(G[t],G[i]),inv^=1;
        res=res*G[i][i]%MOD;
        vector<int>pos;
        F(j,i,m) if(G[i][j]) pos.emplace_back(j);
        ll inv=qpow(MOD-G[i][i],MOD-2)%MOD;
        F(j,i+1,m) if(G[j][i]){
            ll coef=G[j][i]*inv%MOD;
            for(int k:pos) G[j][k]=(G[j][k]+G[i][k]*coef)%MOD;
        }
    }
    return inv?MOD-res:res;
}
signed main(){ 
    ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
    cin>>n;
    F(i,1,n) cin>>a[i],m=max(m,a[i]);
    init();
    F(i,1,n) F(j,1,n) D[i]+=__gcd(a[i],a[j]);
    ll coef=1;
    F(i,1,n-1) coef=coef*D[i]%MOD,D[i]=qpow(D[i],MOD-2);
    F(i,1,m) F(j,1,m) if(i*j/__gcd(i,j)<=m){
        ll qwq=0;
        F(k,1,n-1) if(a[k]%i==0&&a[k]%j==0) (qwq+=D[k])>=MOD&&(qwq-=MOD);
        (G[m-i+1][m-j+1]=((i==j)-phi[j]*qwq)%MOD)<0&&(G[m-i+1][m-j+1]+=MOD);
    }
    cout<<coef*det()%MOD;
    return 0;
}

Day 92

生成子群相关完全不会啊。

2025/02/03 [PA 2024] Grupa permutacji

题意

给定 k 个长为 n 的置换,求它们生成子群的逆序对的平均数对 10^9+7 取模的值。

思路

生成子群最重要的性质是,假设这个生成子群的所有置换中的第 i 位有 k 种取值,那么每种取值的置换个数相同。

这个性质的推论是,选出若干位组成一个向量,这个向量的每种取值对应的置换个数也是相等的。

在本题中,我们可以考虑对所有 i\neq j 的二元组 (i,j) 建点,(i,j) 和所有 (p_i,p_j) 连双向边,则一个连通块中的所有元素就是这两位可能的值。维护连通块中 i<ji>j 的点数,设分别为 a,b,则贡献为 \dfrac{ab}{a+b}

直接维护是 O(n^2k) 的,跑不过去。

前面的 O(n^2) 基本上没法再优化了。我们试图优化一下 O(k)

有一个确定性做法是 Schreier–Sims 算法,但是我不会。你感觉一下,我们随机选出 O(\log n) 个给出置换的子集复合起来,得到的所有置换大概率覆盖了所有上面图的边。

所以复杂度变成了 O(n(n+k)\log n)

代码

#include<bits/stdc++.h>
#define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout)
#define ll long long
#define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i)
#define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i)
using namespace std;
const int MAXN=3001,MOD=1e9+7;
int n,k,a[MAXN][MAXN],dsu[MAXN*MAXN];
inline int id(int i,int j){
    return (i-1)*n+j;
}
int find(int x){
    return x==dsu[x]?x:dsu[x]=find(dsu[x]);
}
mt19937 gen(time(0)^*new int);
ll le[MAXN*MAXN],ge[MAXN*MAXN];
inline ll qpow(ll base,int expo=MOD-2){
    ll res=1;
    while(expo){
        (expo&1)&&(res=res*base%MOD);
        base=base*base%MOD,expo>>=1;
    }
    return res;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>k;
    F(i,1,k) F(j,1,n) cin>>a[i][j];
    iota(dsu+1,dsu+n*n+1,1);
    F(T,1,40){
        static int p[MAXN],pp[MAXN];
        iota(p+1,p+n+1,1);
        F(i,1,k) if(gen()&1){
            F(j,1,n) pp[p[j]]=a[i][j];
            swap(p,pp);
        }
        F(i,1,n) F(j,1,n) if(i!=j) dsu[find(id(p[i],p[j]))]=find(id(i,j));
    }
    F(i,1,n) F(j,1,n) if(i!=j){
        if(i<j) ++le[find(id(i,j))];
        else ++ge[find(id(i,j))];
    }
    int ans=0;
    F(i,1,n) F(j,1,n) if(i!=j&&find(id(i,j))==id(i,j)) ans=(ans+le[id(i,j)]*ge[id(i,j)]%MOD*qpow(le[id(i,j)]+ge[id(i,j)]))%MOD;
    cout<<ans;
    return 0;
}

Day 93

太久没写了,随便找了一道,结果是金牌题……

和数学过情人节。

2025/02/14 [AGC063F] Simultaneous Floor

题意

求把 (a_1,a_2) 变成 (b_1,b_2) 的最小操作次数,一次操作定义为选择一个正实数 x,执行 (a_1,a_2)\leftarrow (\lfloor a_1x\rfloor,\lfloor a_2x\rfloor),或报告无解。

思路

把有序数对看成坐标,扔到平面直角坐标系上研究。下面记点 A(a_1,a_2),B(b_1,b_2)。不加说明时,我们均指第一象限。记值域为 V

发现点 P(x,y) 可以变成形如 (i,\lfloor\dfrac{y}{x}i\rfloor),(\lfloor\dfrac {x}{y}i\rfloor,i),i\in\mathbb Z 的点。不过还是有向下取整,不好描述。

那么倒过来看,对于 Q(p,q),能够到达它的 P 的集合是什么。

设直线 OP:y=kx,如果 P 能到达 Q,则 OP 必须穿过以 Q 为左下角、边长为 1 的正方形,穿过是指 OP 在正方形内的部分的长度 >0

Q_u(p,q+1),Q_r(p+1,q),则只要 OP\angle Q_rOQ_u 内部(不包括 OQ_u,OQ_r),其就会穿过这个正方形。

那么就有 k\in(k_{OQ_r},k_{OQ_u}),即能够到达 Q(p,q)P\in\{(x,y)|\dfrac{q}{p+1}<\dfrac y x<\dfrac{q+1}p\}

l_1=\dfrac{b_2}{b_1+1},r_1=\dfrac{b_2+1}{b_1},我们就可以算出经过 \le 1 次操作到达 B 的点的集合为 S_1=\{(x,y)|l_1<\dfrac y x<r_1\}

同理,设 l_2=\min\{\dfrac y{x+1}|(x,y)\in S_1\},r_2=\max\{\dfrac{y+1}x|(x,y)\in S_1\},则经过 \le 2 次操作到达 B 的点的集合为 S_2=\{(x,y)|l_2<\dfrac y x<r_2\}

更一般地,对于 t\ge 2,设 l_t=\min\{\dfrac y{x+1}|(x,y)\in S_{t-1}\},r_t=\max\{\dfrac{y+1}x|(x,y)\in S_{t-1}\},则经过 \le t 次操作到达 B 的点的集合为 S_t=\{(x,y)|l_t<\dfrac y x<r_t\}

先来研究已知 l_{t-1},r_{t-1} 如何求 l_t,r_t。下面只讨论 l_tr_t 是类似的。

问题相当于求最小的 \dfrac{y}{x+1},满足 l_{t-1}<\dfrac{y}{x}<r_{t-1}。考虑到 \dfrac{y}{x+1}=\dfrac{1}{\frac{x}{y}+\frac{1}{y}},即我们要 y 尽量小 \dfrac{x}{y} 尽量大。在 y 最小的同时取 x 最大即可,可以用反证法和一些缩放证明。

同理,求 r_t 时找满足 x 最小的最大 y 即可。

找到这样的 x,y 可以在 SBT 上二分实现。

接下来就是要求第一次满足 A\in S_tt。打表发现 l_t,r_t 在迭代若干轮之后就不变了,所以可以暴力求每一轮的 l_t,r_t 是多少。考虑到 l,r 在 SBT 上移动的过程,得到 x=1y=1 后就不会变了,所以迭代次数是 O(\log V) 的。

注意有点在坐标轴、y=x 上以及不在 y=x 同一侧的时候判无解。可以用关于 y=x 对称简化讨论。

总的复杂度是 O(T\log^2 V)

代码

#include<bits/stdc++.h>
#define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout)
#define ll long long
#define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i)
#define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i)
using namespace std;
struct Frac{
    ll x,y;
    Frac(const ll&a=0,const ll&b=0):x(a),y(b){}
    inline void reduce(){ll qwq=__gcd(x,y);x/=qwq,y/=qwq;}
    inline Frac inv(){return Frac(y,x);}
    bool operator<(const Frac&qwq)const{return x*qwq.y<y*qwq.x;}
    bool operator==(const Frac&qwq)const{return x==qwq.x&&y==qwq.y;}
};
Frac find(Frac ql,Frac qr){
    Frac l=Frac(0,1),r=Frac(1,0),now=Frac(1,1);
    while(!(ql<now&&now<qr)){
        if(!(ql<now)){
            ll fac=(ql.x*now.y-ql.y*now.x)/(ql.y*r.x-ql.x*r.y)+1;
            now=Frac(now.x+fac*r.x,now.y+fac*r.y),l=Frac(now.x-r.x,now.y-r.y);
        }else{
            ll fac=(qr.y*now.x-qr.x*now.y)/(qr.x*l.y-qr.y*l.x)+1;
            now=Frac(now.x+fac*l.x,now.y+fac*l.y),r=Frac(now.x-l.x,now.y-l.y);
        }
    }
    if(now.y==1) now.x=(qr.x-1)/qr.y;
    now.reduce();
    return now;
}
ll a1,a2,b1,b2;
inline int solve(){
    if(a1>a2) swap(a1,a2),swap(b1,b2);
    Frac a=Frac(a1,a2),b=Frac(b1,b2);
    if(a==b) return 0;
    if(!a1&&!a2) return -1;
    if(!a1) return b1?-1:1;
    if(!b1&&!b2) return 1;
    if(a1==a2) return b1==b2?1:-1;
    if(b1>b2) return -1;
    if(!b1) return 1+(a2<=a1*b2);
    Frac l(b1,b2+1),r(b1+1,b2);
    l.reduce(),r.reduce(),a.reduce();
    if(Frac(1,b2/b1+1)<a){
        int t=1;
        while(!(l<a&&a<r)){
            ++t;
            Frac nl=l.x>1?find(r.inv(),l.inv()).inv():Frac(l.x,l.y-1),nr=r.y>1?find(l,r):Frac(r.x-1,r.y);
            l=nl,r=nr;
            ++l.y,++r.x;
            l.reduce(),r.reduce();
        }
        return t;
    }
    return -1;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int T;
    for(cin>>T;T;--T){
        cin>>a1>>a2>>b1>>b2;
        cout<<solve()<<"\n";
    }
    return 0;
}

Day 94

看过的没写代码的题,回旋镖正中靶心。

2025/02/19 『JROI-4』少女幻葬

题意

给定 n,k,求有多少个长为 n 的序列 a_1,\cdots,a_n,满足 l_i\le a_i\le r_i,\gcd(a_i,a_{i+1})\neq k,\gcd(a_i,a_{i+1},a_{i+2})=k

思路

记值域为 m

由于 \gcd(a_i,a_{i+1},a_{i+2})=k,所以有 k|a_i。不妨令 a_i\leftarrow\dfrac{a_i}{k},l_i\leftarrow\lceil\dfrac{l_i}{k}\rceil,r_i\leftarrow\lfloor\dfrac{r_i}{k}\rfloor。则原限制转化为 l_i\le a_i\le r_i,\gcd(a_i,a_{i+1})\neq 1,\gcd(a_i,a_{i+1},a_{i+2})=1。接下来的 k 和题目里的没有关系。

设一个状态转移会比较抽象。设 f(i,j,k) 表示考虑到第 i 个数,a_i=k\gcd(a_i,a_{i-1})=j 的方案数;设 g(i,j,k) 表示考虑到第 i 个数,a_i=k\gcd(a_i,a_{i+1})=j 的方案数。

首先是 gf 的转移为 f(i,j,k)=\sum\limits_{v=l_{i-1}}^{r_{i-1}}g(i-1,j,v)[\gcd(v,k)=j]

除掉 j 后反演得到 f(i,j,k)=\sum\limits_{v=\lceil\frac{l_{i-1}}{j}\rceil}^{\lfloor\frac{r_{i-1}}{j}\rfloor}g(i-1,j,vj)\sum\limits_{d|v,d|\frac{k}{j}}\mu(d)

即:

f(i,j,k)=\sum\limits_{d|\frac{k}{j}}\mu(d)\sum\limits_{v=\lceil\frac{l_{i-1}}{dj}\rceil}^{\lfloor\frac{r_{i-1}}{dj}\rfloor}g(i-1,j,vdj)

不妨 k\leftarrow \dfrac k j,对第三维做狄利克雷后缀和,对位乘 \mu 后再做狄利克雷前缀和即可转移,时间复杂度 O(nm\log m\log\log m),前一个 \log m 是枚举因数的调和级数,后一个 \log\log m 是狄利克雷前后缀和。

然后再是 fg 的转移为:

g(i,j,k)=\sum\limits_{x|k}f(i,x,k)[\gcd(x,j)=1]

枚举 k,把 f(i,x,k) 贡献到 x 的质因数集合的位置然后求高维后缀和,那么 g(i,j,k) 就是 j 的质因数集合的补集的位置。这一部分复杂度是 \sum\limits_{k\le m}d(k)\omega(k)=\sum\limits_{k\le m}\omega(k)\sum\limits_{i|k}1=\sum\limits_{ik\le m}\omega(ik)\le\sum\limits_{ik\le m}(\omega(i)+\omega(k))=2\sum\limits_{i\le m}\sum\limits_{k\le\frac m i}\omega(i)=2\sum\limits_{i\le m}O(\dfrac m i\log\log\dfrac v i)=O(m\log m\log\log m)。加上枚举第一维,总的复杂度是 O(nm\log m\log\log m)

然后就做完了,时间复杂度 O(nm\log m\log\log m)

代码

#include<bits/stdc++.h>
#define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout)
#define ll long long
#define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i)
#define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i)
using namespace std;
const int MAXN=2001,MAXV=5001,MOD=998244353;
inline void add(int&x,int y){(x+=y)>=MOD&&(x-=MOD);}
int n,m,l[MAXN],r[MAXN],tot;
int pr[MAXV],cnt,vis[MAXV],mu[MAXV],omega[MAXV];
inline void init(){
    mu[1]=1;
    F(i,2,m){
        !vis[i]&&(pr[++cnt]=i,vis[i]=i,mu[i]=-1);
        F(j,1,cnt){
            int t=i*pr[j];
            if(t>m) break;
            vis[t]=pr[j];
            if(vis[i]==pr[j]) break;
            else mu[t]=-mu[i];
        }
    }
    return;
}
vector<int>fac[MAXV];
int id[MAXV][MAXV];
int f[50000],g[50000],tmp[50000],pf[50000];
int sum[130];
signed main(){ 
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int k=0;
    cin>>n>>k;
    F(i,1,n) cin>>l[i]>>r[i],l[i]=(l[i]-1)/k+1,r[i]=r[i]/k,m=max(m,r[i]);
    init();
    F(i,1,m) for(int j=i;j<=m;j+=i) fac[j].push_back(i),id[j][i]=++tot;
    F(i,2,m){
        static int idx[MAXV];
        int now=0,qwq=i,qaq=-1;
        while(qwq!=1){
            if(now!=vis[qwq]) now=vis[qwq],idx[now]=++qaq;
            qwq/=vis[qwq];
        }
        omega[i]=qaq+1;
        for(int j:fac[i]){
            qwq=j;
            while(qwq!=1) pf[id[i][j]]|=(1<<idx[vis[qwq]]),qwq/=vis[qwq];
        }
    }
    F(i,2,m) for(int j=((l[1]-1)/i+1)*i;j<=r[1];j+=i) g[id[j][i]]=1;
    F(i,2,n){
        F(j,2,m){
            for(int k=j,qwq=1;k<=m;k+=j,++qwq) tmp[qwq]=g[id[k][j]];
            int lim=m/j;
            F(k,1,cnt){
                if(pr[k]>lim) break;
                for(int t=lim/pr[k]*pr[k];t>=pr[k];t-=pr[k]) add(tmp[t/pr[k]],tmp[t]);
            }
            F(k,1,lim) (tmp[k]=tmp[k]*mu[k])<0&&(tmp[k]+=MOD);
            F(k,1,cnt){
                if(pr[k]>lim) break;
                for(int t=pr[k];t<=lim;t+=pr[k]) add(tmp[t],tmp[t/pr[k]]);
            }
            for(int k=j,qwq=1;k<=m;k+=j,++qwq) f[id[k][j]]=tmp[qwq];
        }
        F(j,2,m) for(int k=j;k<=m;k+=j) if(k<l[i]||k>r[i]) f[id[k][j]]=0;
        F(k,2,m){
            F(j,0,(1<<omega[k])-1) sum[j]=0;
            for(int j:fac[k]) if(j!=1) add(sum[pf[id[k][j]]],f[id[k][j]]);
            F(j,0,omega[k]-1) F(s,0,(1<<omega[k])-1) if(s>>j&1) add(sum[s],sum[s^(1<<j)]);
            for(int j:fac[k]) if(j!=1) g[id[k][j]]=sum[((1<<omega[k])-1)^pf[id[k][j]]];
        }
    }
    int ans=0;
    F(i,2,m) for(int j=i;j<=m;j+=i) add(ans,f[id[j][i]]);
    cout<<ans;
    return 0;
}

Day 95

一个很厉害的技巧。

2025/02/22 [ABC311Ex] Many Illumination Plans

题意

有一个以 1 为根的有 n 个节点的树,每一个点有权值值 B_i、重量 W_i 和颜色 C_i\in\{0,1\}。要求对于每一个点 u 的子树 T_u 回答以下问题:

可以对 T_u 进行若干次操作,每一次选择一个非根节点,将这个点的所有儿子转移给其父亲,然后删去该节点。操作后应满足 T_u 中不存在两端点同色的边,所有点的重量和小于 X。问操作之后的 T_u 的权值之和的最大值。

思路

先讨论求以 1 为根子树的答案。

删除顺序是不重要的,所以可以当成选几个点满足重量和 \le X、与选中的点里最近的祖先颜色不同,使得权值和最大。也就是背包。

于是设 dp(i,j,k) 表示当前在节点 i 子树中、重量和为 j、最浅的点的颜色为 k 的最大权值和,转移为对于 u 的儿子 vdp(u,*,C_u)dp(u,*,C_u)dp(v,*,1-C_u)\max 卷积。

然后你发现完蛋了,\max 卷积是 O(X^2) 的。但是一个插入数是 O(X) 的,我们只要想办法把 \max 卷积变成插入一个数就好了。

怎么办呢?你发现 \max 卷积具有交换律和结合律,所以我们可以随便改变运算顺序。再加上每次我们初始的时候 dp 数组都为空,这个很浪费。不如每次递归子树时传入现在的“半成品”dp 数组,回溯的时候把当前节点的贡献再加进去。由于 k 有两种取值,我们要 dfs 两次子树,每个点被访问了 O(\text{祖先个数次}),总的复杂度变成了 O(n^2X)。已经比 O(nX^2) 好了!

在下一步我们使用多次递归子树的经典 Trick:轻重子树分治。你考虑第一次递归进去的时候 dp(u,*,0),dp(u,*,1) 都是 \max 卷积的幺元,这两个没有区分,只需要递归一次。所以先递归一次重儿子,轻儿子再递归两次。

分析一下复杂度。设 n 个点的树复杂度为 T(n),重儿子子树大小为 h,其他子树大小为 y_1,\cdots,y_t,则 T(n)=T(h)+O(C)+2\sum\limits_{i=1}^t T(y_i)。显然复杂度高于 O(n),此时根据调整法取 y_1=n-1-h,y_2=\cdots=y_t=0 取到最大 T(h)+2T(n-1-h)。进一步,由于 n-1-h\le h,取 h=\dfrac{n-1}{2} 最优,即 T(n)\le 3T(\dfrac n 2)+O(C)。根据主定理,得到复杂度上界 O(n^{\log_2 3}C)

然后是对每个子树求。你发现上面这个东西一次求了一条重链的答案,所以再遍历一遍轻子树即可,复杂度仍然是 O(n^{\log_2 3}C),其中 \log_2 3\approx 1.582

代码

#include<bits/stdc++.h>
#define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout)
#define ll long long
#define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i)
#define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i)
#define poly vector<ll>
#define fi first
#define se second
using namespace std;
const int MAXN=201,MAXV=50001;
int n,X,W[MAXN],C[MAXN],fa[MAXN],siz[MAXN],son[MAXN];
ll ans[MAXN],B[MAXN];
vector<int>g[MAXN];
void dfs1(int now){
    siz[now]=1;
    for(int i:g[now]) dfs1(i),siz[now]+=siz[i],siz[son[now]]<siz[i]&&(son[now]=i);
    return;
}
poly zero;
vector<poly> dfs2(int now,poly dp,bool heavy){
    if(!heavy) for(int i:g[now]) if(i!=son[now]) dfs2(i,dp,0);
    vector<poly>qwq(2,zero);
    if(son[now]){
        qwq=dfs2(son[now],dp,heavy);
        for(int i:g[now]) if(i!=son[now]) F(j,0,1) qwq[j]=dfs2(i,qwq[j],1)[j];
    }else qwq[0]=qwq[1]=dp;
    R(i,X,W[now]){
        qwq[C[now]][i]=max(qwq[C[now]][i],qwq[C[now]^1][i-W[now]]+B[now]);
        if(!heavy) ans[now]=max(ans[now],qwq[C[now]^1][i-W[now]]+B[now]);
    }
    return qwq;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>X;
    F(i,2,n) cin>>fa[i],g[fa[i]].push_back(i);
    F(i,1,n) cin>>B[i]>>W[i]>>C[i];
    dfs1(1);
    zero=poly(X+1,-5e17);
    zero[0]=0;
    dfs2(1,zero,0);
    F(i,1,n) cout<<ans[i]<<"\n";
    return 0;
}

Day 96

五合一。

2025/02/24 [CEOI 2020] 象棋世界

题意

rc 列的棋盘上,q 组询问兵、车、后、象、王从 (1,c_1)(r,c_r) 的最小步数和达到最小步数的方案数,或判断到达不了。保证 r\ge c

思路

#### 车 $c_1\neq c_r$ 时最小步数为 $2$,方案数为 $2$;否则最小步数为 $1$,方案数为 $1$。 #### 后 先特判一步可以到的情况,这种情况都只有 $1$ 种方案。 剩下的情况不超过两步,枚举每一种情况即可。 以上都是 $O(1)$ 的。 #### 象 先判如果坐标之和奇偶性不同就无解。 否则必然是像踢墙跳一样来回走直到走到 $(x,c_r)$ 满足 $x\ge r$,最优步数 $y$ 就求出来了。 方案数的话,就是在每次转弯的地方少走几步,用隔板法可以算出来是 $\dbinom{y-1+\frac{x-r}{2}-1}{\frac{x-r}{2}}$。 由于 $y\le c$,复杂度 $O(c)$。 #### 王 显然走 $r-1$ 步。下面设 $c_1=a,c_r=b$,我们要求从 $(1,a)$ 到 $(r,b)$ 的方案数,每步可以从 $(x,y)$ 走到 $(x+1,y+1),(x+1,y),(x+1,y-1)$,且不能碰 $(x,0),(x,c+1)$ 这两条线。 这是一个经典问题。设 $f_i$ 表示不考虑不能碰到的方案数,使用反射容斥,得到答案为 $\sum\limits_{k\equiv b-a\pmod{2c+2}}f_k-\sum\limits_{k\equiv -a-b\pmod{2c+2}}f_k$。 注意到 $f_k=[x^k](x+1+x^{-1})^{r-1}$,做循环卷积即可。预处理复杂度 $O(c^2\log r)$ 或 $O(c\log c\log r)$,询问复杂度 $O(1)$。 ### 代码 ```cpp #include<bits/stdc++.h> #define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout) #define ll long long #define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i) #define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i) using namespace std; const int MAXN=4010,MOD=1e9+7; int r,c,q,len; ll fact[MAXN],inv[MAXN]; inline ll C(ll x,ll y){ ll res=inv[y]; F(i,0,y-1) res=res*(x-i)%MOD; return res; } struct Poly{ ll v[MAXN]={}; Poly operator*(const Poly&x)const{ Poly res; F(i,0,len-1) F(j,0,len-1) res.v[(i+j)%(2*c+2)]=(res.v[(i+j)%(2*c+2)]+v[i]*x.v[j])%MOD; return res; } }base,f; signed main(){ ios::sync_with_stdio(0); cin.tie(0),cout.tie(0); cin>>r>>c>>q; len=2*c+2,fact[0]=fact[1]=inv[0]=inv[1]=1; F(i,2,len) fact[i]=fact[i-1]*i%MOD,inv[i]=MOD-MOD/i*inv[MOD%i]%MOD; F(i,2,len) inv[i]=inv[i-1]*inv[i]%MOD; base.v[0]=base.v[1]=base.v[2*c+1]=1; f.v[0]=1; int expo=r-1; while(expo){ (expo&1)&&(f=f*base,1); base=base*base,expo>>=1; } for(;q;--q){ char T; int a,b; cin>>T>>a>>b; switch(T){ case 'P':{ if(a!=b) cout<<"0 0\n"; else cout<<r-1<<" 1\n"; break; }case 'R':{ if(a!=b) cout<<"2 2\n"; else cout<<"1 1\n"; break; }case 'Q':{ if(a==b||abs(b-a)==r-1){ cout<<"1 1\n"; continue; } int ans=4; if(r==c){ if(a==1||a==c) ++ans; if(b==1||b==c) ++ans; } if(((1+a)&1)==((r+b)&1)){ if(a+b+r-1<=2*c) ++ans; if(a+b-r+1>=2) ++ans; } cout<<"2 "<<ans<<"\n"; break; }case 'B':{ if(((1+a)&1)!=((r+b)&1)){ cout<<"0 0\n"; break; } int step=0x7fffffff,pos,x,ans=0; F(O,0,1){ if(O) pos=a,x=1; else pos=c-a+1,x=c; int qaq=(r-pos)/(2*c-2),now=qaq*2; pos+=(2*c-2)*qaq; while(pos<r||x!=b){ ++now; if(x==1){ if(pos+b-1>=r){ pos+=b-1; break; } pos+=c-1,x=c; }else if(x==c){ if(pos+c-b>=r){ pos+=c-b; break; } pos+=c-1,x=1; continue; } } if(step>now) step=now,ans=0; if(step==now){ int d=(pos-r)/2; (ans+=C(d+now-1,d))>=MOD&&(ans-=MOD); } } cout<<step+1<<" "<<ans<<"\n"; break; }case 'K':{ cout<<r-1<<" "<<(f.v[(b-a+len)%len]-f.v[(-b-a+len)%len]+MOD)%MOD<<"\n"; break; } } } return 0; } ``` ## Day 97 在省队名单出之前,赶快写几篇吧。 2025/03/04 [[ARC070F] HonestOrUnkind](https://atcoder.jp/contests/arc070/submissions/63388745) ### 题意 有 $a$ 个好人和 $b$ 个坏人。你可以向 $i$ 询问 $j$ 是不是好人。好人永远说真话,坏人会用某种策略回答。用不超过 $2(a+b)$ 次询问出每个人是好人还是坏人,或判断不可能问出来。 ### 思路 首先如果 $a\le b$,那么坏人中挑 $a$ 个出来互相指认是好人并说其他人都是坏人,这样永远无法区分好坏。 否则 $a>b$。考虑维护一个栈,满足栈里存在一个分界点,往栈顶都是好人,往栈底都是坏人。从小到达枚举 $i$,如果栈空直接入栈,否则向栈顶询问 $i$ 是不是好人。 如果栈顶回答是好人,那么无论如何把 $i$ 入栈都不会破坏栈的性质,直接入栈。 否则,我们不仅不让 $i$ 入栈,还把栈顶弹掉。这样要么是两个坏人消掉了,要么是一个好人一个坏人消掉了,仍然满足好人更多。这里询问次数 $<a+b$。 既然好人始终更多,最后的栈顶就是好人。挨个问一遍这个好人即可。这里询问次数 $a+b-1$,满足条件。 时间复杂度 $O(a+b)$。 ### 代码 ```cpp #include<bits/stdc++.h> #define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i) #define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i) #define ll long long #define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout) using namespace std; const int MAXN=2e5+2; int a,b; inline char ask(int x,int y){ cout<<"? "<<x<<" "<<y<<endl; char ch; cin>>ch; return ch; } int stk[MAXN],tp; char ans[MAXN]; signed main(){ ios::sync_with_stdio(0); cin.tie(0),cout.tie(0); cin>>a>>b; if(a<=b) return cout<<"Impossible",0; F(i,0,a+b-1){ if(!tp){ stk[++tp]=i; continue; } if(ask(stk[tp],i)=='Y') stk[++tp]=i; else --tp; } F(i,0,a+b-1) ans[i]=int(ask(stk[tp],i)=='Y')+'0'; cout<<"! "<<ans; return 0; } ``` ## Day 98 是少见的条件概率题。 2025/03/07 [[CTSC2017] 游戏](https://www.luogu.com.cn/problem/P3772) ### 题意 小 R 和小 B 玩了 n 局游戏。第 $1$ 局游戏小 R 获胜的概率是 $p_1$,小 B 获胜的概率是 $1 −p_1$。如果第 $i − 1$ 局游戏小 R 获胜,那么第 $i$ 局游戏小 R 获胜的概率为 $p_i$,小 B 获胜的概率为 $1 − p_i$;如果第 $i − 1$ 局游戏小 B 获胜,那么第 $i$ 局游戏小 R 获胜的概率为 $q_i$,小 B 获胜的概率为 $1 − q_i$。 你现在有若干条信息,每条形如某一局谁赢了。你需要支持加入信息,删除信息,求以当前所有信息为前提时小 R 的期望获胜局数。 ### 思路 根据期望的线性性,把每一局小 R 赢的条件概率加起来就是所求的条件期望。 然后发现影响第 $i$ 局的信息只有左右两边第一个已知结果的位置 $l,r$。设事件 $L,R$ 分别表示第 $l,r$ 局给定的获胜情况,事件 $X$ 为第 $i$ 局小 R 获胜。所求即为 $P(X|LR)$。 使用条件概率的定义得到 $P(X|LR)=\dfrac{P(LRX)}{P(LR)}=\dfrac{P(L)P(X|L)P(R|X)}{P(L)P(R|L)}=\dfrac{P(X|L)P(R|X)}{P(R|L)}$。 首先是分母。维护一个二维行向量 $[b,r]$ 分别表示小 R 输和赢的概率,每次转移就是右乘上矩阵 $\begin{bmatrix}1-q_i & q_i\\1-p_i & p_i\end{bmatrix}$。发现分母的条件概率相当于是钦定第 $l$ 局的行向量是 $[1,0]$ 或 $[0,1]$,直接维护矩阵区间积即可。 然后是分子。比起分母,分子相当于是把某一局强行钦定为小 R 赢,即把转移矩阵的第一列都改为 $0$。使用矩阵乘法的分配律即可维护。 找 $l,r$ 可以用 set 维护,修改时减去老区间的贡献加上新区间的贡献。为了方便,可以加上第 $0$ 局和第 $n+1$ 局并让小 R 在这两局胜。 时间复杂度 $O(n\log n)$。 ### 代码 ```cpp #include<bits/stdc++.h> #define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i) #define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i) #define ll long long #define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout) using namespace std; const int MAXN=2e5+2; int n,m; double p[MAXN],q[MAXN]; struct Mat{ double v[2][2]; Mat(const double&a=0,const double&b=0,const double&c=0,const double&d=0){v[0][0]=a,v[0][1]=b,v[1][0]=c,v[1][1]=d;} Mat operator+(const Mat&x)const{return Mat(v[0][0]+x.v[0][0],v[0][1]+x.v[0][1],v[1][0]+x.v[1][0],v[1][1]+x.v[1][1]);} Mat operator*(const Mat&x)const{return Mat(v[0][0]*x.v[0][0]+v[0][1]*x.v[1][0],v[0][0]*x.v[0][1]+v[0][1]*x.v[1][1],v[1][0]*x.v[0][0]+v[1][1]*x.v[1][0],v[1][0]*x.v[0][1]+v[1][1]*x.v[1][1]);} }; struct Node{ Mat ch,mo; Node operator*(const Node&x)const{return (Node){ch*x.mo+mo*x.ch,mo*x.mo};} }segt[MAXN<<2]; #define lc (now<<1) #define rc (now<<1|1) #define mid ((l+r)>>1) #define ls lc,l,mid #define rs rc,mid+1,r void build(int now,int l,int r){ if(l==r){ segt[now].mo=Mat(1-q[l],q[l],1-p[l],p[l]); segt[now].ch=Mat(0,0,1-p[l],p[l]); return; } build(ls),build(rs); segt[now]=segt[lc]*segt[rc]; return; } Node qry(int now,int l,int r,int ql,int qr){ if(ql<=l&&r<=qr) return segt[now]; if(qr<=mid) return qry(ls,ql,qr); else if(ql>mid) return qry(rs,ql,qr); else return qry(ls,ql,qr)*qry(rs,ql,qr); } set<int>info; bool win[MAXN]; double ask(int l,int r){ Node qwq=qry(1,1,n+1,l+1,r); return qwq.ch.v[win[l]][win[r]]/qwq.mo.v[win[l]][win[r]]; } signed main(){ ios::sync_with_stdio(0); cin.tie(0),cout.tie(0); cout<<fixed<<setprecision(6); char type; cin>>n>>m>>type; cin>>p[1]; F(i,2,n) cin>>p[i]>>q[i]; win[0]=win[n+1]=1; p[n+1]=q[n+1]=1; build(1,1,n+1); info.insert(0),info.insert(n+1); double ans=ask(0,n+1); for(;m;--m){ char op[4]; int pos; cin>>op>>pos; if(op[0]=='a'){ cin>>win[pos]; auto it=info.lower_bound(pos); int l=*prev(it),r=*it; info.insert(pos); ans-=ask(l,r),ans+=ask(l,pos),ans+=ask(pos,r); }else{ info.erase(pos); auto it=info.lower_bound(pos); int l=*prev(it),r=*it; ans-=ask(l,pos),ans-=ask(pos,r),ans+=ask(l,r); } cout<<ans-1<<"\n"; } return 0; } ``` ## Day 99 完成立下的 flag。 2025/03/09 [[湖北省选模拟 2025] 团队协作 / team](https://www.luogu.com.cn/problem/P11824) ### 题意 对每个点求出包含这个点的所有独立集中点权最大值的和,对 $998244353$ 取模。 ### 思路 我们称节点 $i$ 的点权比节点 $j$ 大当且仅当 $v_i>v_j$ 或 $v_i=v_j,i>j$。 考虑转置原理: **【转置原理】** 对于一个线性算法,我们可以通过倒序执行所有操作并把操作 $x\leftarrow x+ky$ 改为 $y\leftarrow y+kx$ 得到转置问题的算法,时间复杂度不变。 设 $a_{i,j}$ 表示包含点 $i$ 且点权最大的点为 $j$ 的独立集个数,$n$ 阶方阵 $A=(a_{i,j})$,列向量 $\vec{a}=[v_1,v_2,\cdots,v_n]^{\top}$,则答案向量为 $\vec b=A\vec a$。 它的转置问题即为:对于每个 $i$,求满足 $i$ 是独立集中点权最大的点的所有独立集的点权和。 按点权从小到大加入每个点,加入点 $i$ 时所有独立集点权和的变化量就是 $i$ 的答案。在这里,一个点被加入指的是可以在独立集中出现,未被加入指的是不能出现在独立集中。 这是一个经典的 ddp 问题,我们使用静态 top tree 解决。对每个簇设 $f(0/1,0/1),g(0/1,0/1)$ 分别表示不选/选上界点、不选/选下界点的独立集方案数和权值和,转移是显然的。值得注意的是,此处 $g(1,*)$ 我们不计入上界点的贡献。可以新建一个虚点 $0$ 作为 $1$ 的父亲方便统计答案。 如果把 $f$ 视为常量(矩阵里的量),则转移关于 $g$ 是线性的。 需要注意,你可以把 ddp 的每次修改看成持久化的,即我们修改的时候变量所占用的内存都是不同的,需要先赋值一次。 然后转置就行了。时间复杂度 $O(n\log n)$。发现我们实际上并不需要持久化,每次直接覆盖就行,所以空间 $O(n)$。如果还是不明白可以看代码,注释里标了转置前后的内容。 事实上,用转置原理得到的做法和其他几篇直接用 top tree 得到的做法是一样的。 ### 代码 ```cpp #include <bits/stdc++.h> #define File(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout) #define ll long long #define F(i,a,b) for(int i(a),i##i##end(b);i<=i##i##end;++i) #define R(i,a,b) for(int i(a),i##i##end(b);i>=i##i##end;--i) #define fi first #define se second using namespace std; const int MAXN=3e5+2,MOD=998244353; int n; pair<int,int>val[MAXN]; int cnt; struct Node{ short type;//-1unit,0rake,1compress int up,dn,mid; int siz,lc,rc,fa; ll f[2][2],g[2][2]; Node(const int&t=-1,const int&d=0,const int&e=0,const int&a=1,const int&b=0,const int&c=0){ type=t,siz=a,lc=b,rc=c,up=d,dn=e,fa=0; memset(f,0,sizeof(f)),memset(g,0,sizeof(g)); return; } }node[MAXN<<1]; bool isin[MAXN]; inline void upd(int now){ Node&qwq(node[now]),&l(node[qwq.lc]),&r(node[qwq.rc]); memset(qwq.f,0,sizeof(qwq.f)); if(node[now].type) F(i,0,1) F(j,0,1) F(k,0,isin[l.dn]) qwq.f[i][j]=(qwq.f[i][j]+l.f[i][k]*r.f[k][j])%MOD; else F(i,0,1) F(j,0,1) F(k,0,isin[r.dn]) qwq.f[i][j]=(qwq.f[i][j]+l.f[i][j]*r.f[i][k])%MOD; return; } inline void psd(int now){ Node&qwq(node[now]),&l(node[qwq.lc]),&r(node[qwq.rc]); if(node[now].type){ /* 转置前 F(i,0,1) F(j,0,1) F(k,0,isin[l.dn]){ qwq.g[i][j]=(qwq.g[i][j]+l.f[i][k]*r.g[k][j])%MOD; qwq.g[i][j]=(qwq.g[i][j]+l.g[i][k]*r.f[k][j])%MOD; } */ F(i,0,1) F(j,0,1) F(k,0,isin[l.dn]){ r.g[k][j]=(r.g[k][j]+l.f[i][k]*qwq.g[i][j])%MOD; l.g[i][k]=(l.g[i][k]+r.f[k][j]*qwq.g[i][j])%MOD; }//倒不倒序不影响 }else{ /* 转置前 F(i,0,1) F(j,0,1) F(k,0,isin[r.dn]){ qwq.g[i][j]=(qwq.g[i][j]+l.f[i][j]*r.g[i][k])%MOD; qwq.g[i][j]=(qwq.g[i][j]+l.g[i][j]*r.f[i][k])%MOD; } */ F(i,0,1) F(j,0,1) F(k,0,isin[r.dn]){ r.g[i][k]=(r.g[i][k]+l.f[i][j]*qwq.g[i][j])%MOD; l.g[i][j]=(l.g[i][j]+r.f[i][k]*qwq.g[i][j])%MOD; } } memset(qwq.g,0,sizeof(qwq.g)); return; } inline int merge(int x,int y,int type){ node[x].fa=node[y].fa=++cnt; node[cnt]=Node(type,node[x].up,node[type?y:x].dn,node[x].siz+node[y].siz,x,y); return upd(cnt),cnt; } #define Poi vector<int>::iterator int build(Poi l,Poi r,int type){ if(l==r) return 0; if(l+1==r) return *l; int sum=0,all=0; for(auto it=l;it!=r;++it) all+=node[*it].siz; Poi mid=l+1; for(auto it=l;it!=r;++it){ sum+=node[*it].siz; if(sum*2<=all) mid=it+1; else break; } return merge(build(l,mid,type),build(mid,r,type),type); } int siz[MAXN],fa[MAXN],son[MAXN],rt[MAXN];//根为 cnt vector<int>g[MAXN]; void dfs1(int now){ siz[now]=1; node[now]=Node(-1,fa[now],now); node[now].f[0][0]=node[now].f[1][0]=node[now].f[0][1]=1; isin[now]=1; for(int i:g[now]){ dfs1(i); siz[now]+=siz[i]; if(siz[son[now]]<siz[i]) son[now]=i; } return; } void dfs2(int now,bool heavy){ if(son[now]) dfs2(son[now],1); for(int i:g[now]) if(i!=son[now]) dfs2(i,0); if(!heavy){ vector<int>chain; chain.push_back(now); for(int i(son[now]);i;i=son[i]){ vector<int>sub({i}); for(int j:g[fa[i]]) if(j!=i) sub.push_back(rt[j]); chain.push_back(build(sub.begin(),sub.end(),0)); } rt[now]=build(chain.begin(),chain.end(),1); } return; } int ans[MAXN]; signed main(){ ios::sync_with_stdio(0); cin.tie(0),cout.tie(0); cin>>n; F(i,2,n) cin>>fa[i],g[fa[i]].push_back(i); F(i,1,n) cin>>val[i].fi,val[i].se=i; sort(val+1,val+n+1); dfs1(1),cnt=n,dfs2(1,0); R(i,n,1){//倒序执行 /* 转置前 (ans[i]-ans[i-1])+=node[cnt].g[0][0/1] 转置后倒序变成 node[cnt].g[0][0/1]+=val[i]-val[i+1] */ node[cnt].g[0][0]=(node[cnt].g[0][0]+val[i].fi-val[i+1].fi+MOD)%MOD; if(isin[node[cnt].dn]) node[cnt].g[0][1]=(node[cnt].g[0][1]+val[i].fi-val[i+1].fi+MOD)%MOD; int now=val[i].se,tp=0; static int path[MAXN]; while(node[now].fa) path[++tp]=node[now].fa,now=path[tp]; R(j,tp,1) psd(path[j]);//从上到下倒序执行 now=val[i].se; ans[now]=node[now].g[0][1],node[now].g[0][1]=0;//node[now].g[0][1]=val[now]的转置 isin[now]=0,node[now].f[0][1]=0; while(node[now].fa) upd(node[now].fa),now=node[now].fa;//按原顺序更新常量(矩阵里的量) } F(i,1,n) cout<<ans[i]<<" "; return 0; } ``` ## Day 100 现在不能公开。 ## 后记 100 天蜜月日记完结撒花!