10月杂题题解

发现这里面好多题都是重量级。。。(现在已经是只加重量级了)

CF814E An unavoidable detour for home

其实是对这篇 题解 的一些理解。

Part 1

不难发现最终图大致长这样:

考虑一棵最短路树,以结点 1 为根,往下每一层有若干个结点,表示最短路距离相同的一些编号连续的结点。

其中每一层内部可以自由连边。

除了每层内部的连边和树边,其余边不合法。

Part 2

考虑第 \(i\) 层的情况。假设第 \(i-1\) 层往下连了 \(j\) 条边,那么说明第 \(i\) 层有 \(j\) 个结点。

设当前层有 \(x\) 个度数为 2 的点,\(y\) 个度数为 3 的点。减去上一层连边的度数,实际有 \(x\) 个度数为 1 的点,\(y\) 个度数为 2 的点。

考虑把度数为 2 的点拆为 2 个度数为 1 的点。

设内部连了 \(z\) 条边,那么给下一层边的方案为:\((x+2y-2z)!\)

考虑如何计算 \(x+2y\) 个点连 \(z\) 条边的方案。

先让 \(x+2y\) 个点选 \(2z\) 个任意排列,钦定相邻两两一对连边。

取消相邻两点的顺序,例如边 \((1,2),(2,1)\) 是等价的。考虑所有可能重复的情况,就是 \(2^z\)

取消边之间的排列顺序,即 \(z!\)

即:\({\Large \frac{(x+2y)!}{(x+2y-2z)!\times 2^z \times z!}}\)

由于拆了点,所以拆的点之间会出现重边和自环。

枚举出现了 \(p\) 条重边,\(q\) 个自环,进行容斥。

设这些不合法的边涉及到的边数 \(s=2p+q\)

\(y\) 个度数为 \(2\) 的点选出这些边的方案:\({\Large \frac{y!}{(y-s)!\times p! \times q!}}\)

就是选 \(s\) 个点,前 \(2p\) 个相邻两两一对,后 \(q\) 个每个连自环。取消一下顺序,然后不取消相邻两点的顺序就是重边了,原理和上面差不多。

那么现在选了 \(s\) 条边,\(2s\) 个点(因为拆点了),还剩 \(x+2y-2s\) 个点,\(z-s\) 条边。

所以还要乘上选择这 \(z-s\) 条边的方案,这个怎么算上面已经提过多次了。

注意还要取消拆点的顺序,还是考虑所以可能重复的情况,就是 \(2^y\)

记得容斥一下,那么有 \(x\) 个度数为 \(1\) 的点,\(y\) 个度数为 \(2\) 的点,内部连 \(z\) 条边并给下一层 \(x+2y-2z\) 条边的方案为:

\({\Large \sum\limits_{s=2p+q ,s\leq \min(y,z)}} {\Large \frac{(-1)^{{p+q}} \times y!}{(y-s)!\times p!\times q!}\times \frac{(x+2y-2s)!}{(x-2y-2z)!\times 2^{z-s}\times (z-s)!}\times (x+2y-2z)!\times \frac{1}{2^y}}\)

Part 3

\(t_s={\Large \sum\limits_{s=2p+q} \frac{(-1)^{p+q}}{p! \times q!}}\)

则原式可以写为:

\({\Large \sum\limits_{0\leq s \leq \min(y,z)}^{} \frac{t_s\times y!\times (x+2y-2s)!}{(y-s)!\times 2^y} \times \frac{1}{(z-s)!\times 2^{z-s}}}\)

\(f(i,j)\) 表示考虑第 \(i\) 个点,向下一层连了 \(j\) 条边的方案数。

那么可以枚举 \(z,s\) 转移到 \(f(i+j,x+2y-2z)\)。但是这样是 \(O(n^4)\) 的。

注意上面那个式子,前面只需要枚举 \(s\),后面枚举 \(z-s\) 就能转移。

那么记一个辅助数组 \(g(i,j)\),先枚举 \(s\)\(f(i,j)\) 转移到 \(g(i+j,x+2y-2s)\),再枚举 \(z-s\) 转移到 \(f(i+j,x+2y-2z)\)

时间复杂度 \(O(n^3)\),空间复杂度 \(O(n^2)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=1005,p=1e9+7,iv2=(p+1)/2;
int n,d[N],f[N][N*2],g[N][N*2];
int t[N],fac[N*2]={1},ifac[N*2],ipw[N*2]={1};
int qpow(int a,int b) {int r=1;for(;b;b>>=1,a=a*a%p) if(b&1) r=r*a%p;return r;}
int inv(int x) {return qpow(x,p-2);}
void inc(int &x,int y) {if((x+=y)>=p) x-=p;}
void mul(int &x,int y) {if((x*=y)>=p) x%=p;}

void init()
{
    for(int i=1;i<=2000;i++)
        fac[i]=fac[i-1]*i%p,
        ipw[i]=ipw[i-1]*iv2%p;
    ifac[2000]=inv(fac[2000]);
    for(int i=2000-1;~i;i--) ifac[i]=ifac[i+1]*(i+1)%p;
}

signed main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>d[i];
    init();
    for(int s=0;s<=n;s++)
        for(int x=0;x*2<=s;x++)
        {
            int y=s-x*2;
            inc(t[s],ifac[x]*ifac[y]%p*(((x+y)&1)?p-1:1)%p);
        }
    f[1][d[1]]=1;
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=i*2;j++)
        {
            //g[i][j]->f[i][j-2(z-s)]
            if(!g[i][j]) continue;
            for(int k=0;k*2<=j;k++)//枚举 z-s
                inc(f[i][j-k*2],g[i][j]*ipw[k]%p*ifac[k]%p);
        }
        for(int j=1;j<=n-i;j++)
        {
            if(!f[i][j]) continue;
            int x=0,y=0;
            for(int k=i+1;k<=i+j;k++) d[k]==2?x++:y++;
            for(int s=0;s<=y;s++) inc(g[i+j][x+2*y-2*s],fac[y]*fac[x+2*y-2*s]%p*t[s]%p*ifac[y-s]%p*ipw[y]%p*f[i][j]%p);
        }
    }
    cout<<f[n][0];
}

C0930 T4 铺设道路

随便补的一场 C 组模拟赛,前三题太水,T4 还有点意思。

神仙贪心。

考虑求出 \(a\) 的差分数组 \(b\)

那么每次操作相当于选择一对 \(l,r\),使 \(b_l -1,b_r+1\),代价是 \((r-l)^2\)

最少天数显然是 \(\sum \max(b_i,0)\)

对于一个位置 \(b_i<0\),寻找前面的 \(b_l\),并操作使得 \(b_i=0\)

由于是差分数组,显然每个 \(b_i<0\) 都能找到对应的一些 \(b_l\),但最后会剩一些 \(b_i\)\(b_i>0\)

这时只需要令 \(r=n+1,b_r=inf\) 即可,然后消去这些 \(b_i>0\)

剩下的 \(b_i\) 的和显然是不变的。

对于 \(b_i>0\),肯定是留在最后消代价最大。

那么应该让消去剩下这些 \(b_i\) 的代价最大/小。

这里考虑令代价最大的情况,那每次对于 \(b_i<0\) 选择离其最近的 \(b_l\),也就是尽可能留住那些离 \(n+1\) 远的 \(b_i>0\)

这个过程可以用队列维护。

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=3e5+5,p=1e9+7;
int n,t,ans1,ans2,a[N];
deque<pair<int,int>> q1,q2;

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=n;i;i--) a[i]-=a[i-1];
    for(int i=1;i<=n;i++) t+=max(a[i],0ll);
    a[n+1]=-1e18;
    for(int i=1;i<=n+1;i++)
    {
        if(a[i]>0) q1.push_back({a[i],i}),q2.push_back({a[i],i});
        else
        {
            int x=-a[i];
            while(x&&q1.size())
            {
                int &y=q1.front().first,l=q1.front().second;
                if(x<y) y-=x,ans1=(ans1+(i-l)*(i-l)%p*x)%p,x=0;
                else ans1=(ans1+(i-l)*(i-l)%p*y)%p,x-=y,q1.pop_front();
            }
            x=-a[i];
            while(x&&q2.size())
            {
                int &y=q2.back().first,l=q2.back().second;
                if(x<y) y-=x,ans2=(ans2+(i-l)*(i-l)%p*x)%p,x=0;
                else ans2=(ans2+(i-l)*(i-l)%p*y)%p,x-=y,q2.pop_back();
            }
        }
    }
    cout<<t<<'\n'<<ans2<<'\n'<<ans1<<'\n';
}

CF1799H Tree Cutting

又一道神仙 dp。话说为什么 3200 是紫啊。。。

无非分为两种操作:保留一个子树,或删除一个子树。

k 很小,考虑状压。

\(f(u,S)\) 表示考虑子树 \(u\),完成了 \(S\) 操作的方案数。

但是保留或者删除一个子树会影响,比如不能同时保留两棵子树。

发现只要有保留子树的操作,那么就只需要独立考虑这个子树了。

多加一维状态,设 \(f(u,i,S)\) ,最早进行保留子树的操作是 \(i\)

\(i=k+1\) 则表示没有保留操作。

考虑合并 \(u,v\) 的答案:

\(v\) 完成的操作集合为 \(T\)

那么转移合法当且仅当:

\(\begin{cases} S \cap T =\varnothing \\ i_u =k+1 \lor i_v=k+1 \\ i_u \neq k+1 \land \forall x \in T < i_u \end{cases}\)

第二个就是避免保留两棵不同子树的情况。

第三个就是对于非子树 u 内的操作要在保留 u 子树之前,当然也可以反着。

然后考虑加入 \((u,fa_u)\) 这条边的贡献。

要么是保留了 \(u\) 整棵子树,要么是删除了 \(u\) 这棵子树。

枚举当前对应的操作是 \(j\)

  • 如果是删除子树,那么 \(i_u=k+1 \land \forall x\in S < j\)
  • 如果是保留子树,那么 \(j< i_u\)

当然还有最基本的子树大小限制。考虑对于 \(s_i\),实际上是删除了 \(s_{i-1}-s_{i}\) 大小的子树。

那么枚举 \(k\in S\) 就可以算出当前子树的大小了。

实际写起来是有很多技巧的,需要慢慢体会。

#include<bits/stdc++.h>
using namespace std;

const int N=5005,K=(1<<6)+1,p=998244353;
int n,k,ans,a[N],sz[N],mx[N],f[N][8][K],g[8][K];
vector<int> G[N];
void inc(int &x,int y) {if((x+=y)>=p) x-=p;}

void dfs(int u,int fa)
{
    f[u][k+1][0]=sz[u]=1;
    for(int v:G[u])
    {
        if(v==fa) continue;
        dfs(v,u);
        sz[u]+=sz[v];
        memset(g,0,sizeof(g));
        for(int i=1;i<=k+1;i++)
            for(int S=0;S<1<<k;S++)
            {
                int rS=(1<<k)-1-S;
                for(int T=rS;;T--,T&=rS)
                {
                    if(mx[T]<i) inc(g[i][S|T],1ll*f[u][i][S]*f[v][k+1][T]%p);
                    if(mx[S]<i&&i!=k+1) inc(g[i][S|T],1ll*f[u][k+1][S]*f[v][i][T]%p);
                    if(!T) break;
                }
            }
        for(int i=1;i<=k+1;i++)
            for(int S=0;S<1<<k;S++)
                f[u][i][S]=g[i][S];
    }
    if(u!=1)
    {
        for(int i=1;i<=k+1;i++)
            for(int S=0;S<1<<k;S++)
                g[i][S]=f[u][i][S];
        for(int i=1;i<=k+1;i++)
            for(int S=0;S<1<<k;S++)
                if(f[u][i][S])
                    for(int j=1;j<i;j++)
                        if(!(S&(1<<j-1)))
                        {
                            int _sz=sz[u];
                            for(int k=1;k<j;k++) if(S&(1<<k-1)) _sz-=a[k-1]-a[k];
                            if(_sz==a[j]) inc(g[j][S|(1<<j-1)],f[u][i][S]);
                            if(_sz==a[j-1]-a[j]&&mx[S]<j) inc(g[i][S|(1<<j-1)],f[u][i][S]);
                        }
        for(int i=1;i<=k+1;i++)
            for(int S=0;S<1<<k;S++)
                f[u][i][S]=g[i][S];
    }
}

int main()
{
    cin>>n;
    for(int i=1;i<n;i++)
    {
        int x,y;cin>>x>>y;
        G[x].push_back(y),G[y].push_back(x);
    }
    cin>>k;a[0]=n;
    for(int i=1;i<=k;i++) cin>>a[i];
    for(int i=1;i<1<<k;i++) for(int j=1;j<=k;j++) if(i&(1<<j-1)) mx[i]=j;
    dfs(1,0);
    for(int i=1;i<=k+1;i++) inc(ans,f[1][i][(1<<k)-1]);
    cout<<ans;
}

AGC028E High Elements

众所周知 noip T3 一般是 3500,而昨天模拟赛是 AT 4100,很合理。

又是神仙 dp + 神仙结论。

当然是贺的这篇题解

由于让字典序最小,从前往后考虑,看第 \(i\) 位是否可以为 0。

设原排列里的前缀最大值为旧的,否则为新的。

如果一种方案合法,必然可以使得 \(x,y\) 两序列中其中一个的前缀最大值全是旧的。

因为如果 \(x,y\) 序列中都有一个新的,交换这两个新的就能消掉。

考虑这个结论有什么用。

先假设 \(x\) 序列里的前缀最大值全是旧的。

\(x\) 之前有 \(cx\) 个前缀最大值,\(y\) 之前有 \(cy\) 个。\(p_{i...n}\) 中有 \(s\) 个旧的,\(y\) 之后有 \(k\) 个旧的,\(m\) 个新的,那么:

\(cx+s-k=cy+k+m\)

\(cx-cy+s=2k+m\)

左边是常数。右边就相当于:令旧的权值为 \(2\),新的权值为 \(1\),能否在之后找到一个前缀最大值序列,使值等于 \(cx-cy+s\)

可以发现如果前缀最大值序列里有旧的,那么不选这个旧的也不会影响合法性,只会令权值 \(-2\)

即若能凑出 \(t\),那么也能凑出 \(t-2\)

所以只用关心权值为奇/偶数的能凑出的最大值。这个可以用线段树优化 dp 维护。

上述是假设 \(x\) 的前缀最大值的全是旧的情况,\(y\) 的情况类似。

最后如果 \(cx \neq cy\), 则无解。

#include<bits/stdc++.h>
using namespace std;

const int N=2e5+5;
int n,ans[N],p[N],s[N],old[N];

#define lc (k<<1)
#define rc (k<<1|1)
#define mid (l+r>>1)
int Mx[N<<2][2];// 0:even  1:odd
void upd(int p,int x,int op,int k=1,int l=1,int r=n)
{
    if(l==r) {Mx[k][op]=x;return;}
    p<=mid?upd(p,x,op,lc,l,mid):upd(p,x,op,rc,mid+1,r);
    Mx[k][op]=max(Mx[lc][op],Mx[rc][op]);
}
int qmax(int x,int y,int op,int k=1,int l=1,int r=n)
{
    if(l>=x&&r<=y) return Mx[k][op];
    int res=-1e9;
    if(x<=mid) res=qmax(x,y,op,lc,l,mid);
    if(mid<y) res=max(res,qmax(x,y,op,rc,mid+1,r));
    return res;
}
bool chk(int i,int w)
{
    if(w<0) return 0;
    return qmax(i,n,w&1)>=w;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++) cin>>p[i];
    int mx=0;
    for(int i=1;i<=n;i++) if(p[i]>mx) mx=p[i],old[i]=1;
    for(int i=n;i>=1;i--) s[i]=s[i+1]+old[i];
    for(int i=1;i<=n*4;i++) Mx[i][1]=-1e9;
    for(int i=n;i>=1;i--)
    {
        int t0=qmax(p[i],n,0),t1=qmax(p[i],n,1);
        if(old[i]) upd(p[i],t0+2,0),upd(p[i],t1+2,1);
        else upd(p[i],t1+1,0),upd(p[i],t0+1,1);
    }
    int cx=0,cy=0,mxx=0,mxy=0;
    for(int i=1;i<=n;i++)
    {
        upd(p[i],0,0),upd(p[i],-1e9,1);
        if(chk(mxy,cx+(p[i]>mxx)-cy+s[i+1])||chk(max(mxx,p[i]),cy-cx-(p[i]>mxx)+s[i+1]))
            ans[i]=0,cx+=p[i]>mxx,mxx=max(p[i],mxx);
        else ans[i]=1,cy+=p[i]>mxy,mxy=max(p[i],mxy);
    }
    if(cx!=cy) puts("-1");
    else for(int i=1;i<=n;i++) cout<<ans[i];
}

P3343 地震后的幻想乡

你能想象这是一次 noip 模拟赛的 T2。

想了半天,打开洛谷题解一看,最高票是_rqy的,一堆密密麻麻的积分差点把我吓跑。

据说有三种解法,然而我只学会了一种最辣鸡的凡人解法。

Part 1

简略证一下这个提示:

  • 对于 \(n\)\([0,1]\) 之间随机变量 \(x_1,x_2...,x_n\),第 \(k\) 小的那个期望值是 \(\frac{k+1}{n}\)

考虑引入第 \(n+1\)\([0,1]\) 之间的随机变量,那么求的等价于这个数小于等于第 \(k\) 小的数的概率。

考虑对这 \(n\) 个数排序,那么可能的情况就是把第 \(n+1\) 个数插入到第 \(1...k\) 个数之前。

总共可以插入 \(n\) 个位置,那么概率为 \(\frac{k+1}{n}\)

Part 2

利用这个提示,考虑比较暴力的做法。

假设我们知道这 \(m\) 条边的大小关系,然后跑 kruskal。

从小到大一条一条加边,直到加入第 \(i\) 条边,图连通了。

也就是说加入前 \(i-1\) 条边图都是不连通的。

那么这个最小生成树的最大边权的排名就是 \(i\)

考虑计算最小生成树中的最大边权的期望排名。

设这条边在 \(m\) 条边中排名为 \(i\) 的概率为 \(P(i)\),也就是加入第 \(i\) 条边图连通的概率,那么答案为:

\(\frac{1}{m+1} \sum\limits_{1\leq i \leq m} i\cdot P(i)\)

等价于:

\(\frac{1}{m+1} \sum\limits_{1\leq i \leq m} \sum\limits_{i \leq j\leq m} P(j)\)

一开始我这个傻逼还不理解为什么,然后这不就是拆成最朴素求和的形式吗?

发现 \(\sum\limits_{i \leq j\leq m} P(j)\) 就是加入前 \(i-1\) 条边图不连通的概率。

可以计算加入前 \(i\) 条边图不连通的方案数,再除以总方案数。

根据上,不难定义状态:\(f(S,i)\) 表示当前点集为 \(S\),加入了 \(i\) 条边,图不连通的方案数。

考虑如何转移。

可以枚举一个连通的子集 \(T\),得到另一个子集 \(S'=S \operatorname{xor} T\),规定 \(T\)\(S'\) 之间不连边,然后 \(S'\) 内可以任意连。

但如果存在一对 \(T_i \in S_j'\),就会算重。

一个计数的套路,以某个 \(S\) 内的点为基准点,规定枚举的 \(T\) 必须包含这个基准点。

画画图大概就知道这样不重不漏了?

Part 3

上述要枚举连通的子集 \(T\),也就是还要计算连通的方案数。

\(f(S,i,0/1)\) 表示点集 \(S\),加入 \(i\) 条边,图不连通/连通的方案。

显然有 \(f(S,i,0)+f(S,i,1)=C_{sz_S}^{i}\),不难列出转移方程:

\(f(S,i+j,0)=\sum f(T,i,0)\cdot C_{sz_{S'}}^{j}\)

\(f(S,i,1)=C_{sz_S}^{i}-f(S,i,0)\)

答案为

\(\large{\frac{1}{m+1}\sum \frac{f(2^n-1,i,0)}{C_{m}^{i}}}\)

#include<bits/stdc++.h>
#define int long long
using namespace std;

double ans;
int n,m,u[50],v[50];
int C[50][50],sz[1100],f[1100][50][2];

signed main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++) cin>>u[i]>>v[i];
    for(int i=0;i<1<<n;i++) for(int j=1;j<=m;j++) if((i&(1<<u[j]-1))&&(i&(1<<v[j]-1))) sz[i]++;
    for(int i=0;i<=m;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];}
    for(int S=0;S<1<<n;S++)
    {
        int k=S&(-S);
        for(int T=(S-1)&S;T;T--,T&=S)
        {
            if(!(T&k)) continue;
            for(int i=0;i<=sz[T];i++)
                for(int j=0;j<=sz[S^T];j++)
                    f[S][i+j][0]+=f[T][i][1]*C[sz[S^T]][j];
        }
        for(int i=0;i<=sz[S];i++) f[S][i][1]=C[sz[S]][i]-f[S][i][0];
    }
    for(int i=0;i<m;i++) ans+=1.0*f[(1<<n)-1][i][0]/C[m][i];
    printf("%.6f",ans/(m+1));
}

CF605E Intergalaxy Trips

相对简单,但本人期望太垃圾,还是记一记。

套路的,设 \(f(i)\) 表示 \(i \rightarrow n\) 的期望天数。

因为是最优策略,所以每次一定是选最优的转移。

假设当前 \(j\) 最优,那么对于 \(\forall f(k)<f(j)\)\(i\) 一定是去不了 \(k\) 的。

题意是可以走自环的,所以如果不存在 \(f(j)<f(i)\),那么就走自环。

\(g(i)=\prod\limits_{j}^{f(j)<f(i)} 1-p_{i,j}\)

那么有:

\(f(i)=\sum\limits_{j}^{f(j)<f(i)} f(j)\times p_{i,j}\times g(j)+f(i)\times g(i)+1\)

移项得:

\(f(i)=\Large{\frac{\sum\limits_{j}^{f(j)<f(i)} f(j)\times p_{i,j}\times g(j)+1}{1-g(i)}}\)

然后你发现之后无论如何 \(f(j)<f(i)\),也就是 \(i\) 不会再成为 \(j\) 的决策。

所以这个 dp 其实是有顺序的,每次找最小的 \(f(i)\) 更新其他的值,类似 dijkstra 算法。

细节上由于 \(1-g(i)\) 会改变,所以 \(f(i)\) 只能记录上面式子的分子,以及特判 \(n=1\) 的情况。

然后最坑的一点,直接读入 double 非常慢,会 TLE。(怪不得题目不直接输入 double 类型)

#include<bits/stdc++.h>
using namespace std;

const int N=1005;
int n,x,vis[N];
double f[N],g[N],p[N][N];

int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            cin>>x,p[i][j]=x/100.0;
    for(int i=1;i<=n;i++) f[i]=1,g[i]=1-p[i][n];
    if(n==1) {cout<<0;return 0;}
    vis[n]=1;
    for(int i=1;i<=n;i++)
    {
        int x=0;
        double mn=1e18;
        for(int j=1;j<=n;j++)
            if(!vis[j]&&f[j]<mn*(1-g[j]))
                x=j,mn=f[j]/(1-g[j]);
        vis[x]=1;
        if(x==1) break;
        for(int j=1;j<=n;j++)
            f[j]+=mn*p[j][x]*g[j],g[j]*=1-p[j][x];
    }
    printf("%.10f",f[1]/(1-g[1]));
}

ABC245H Product Modulo 2

小难数学题。

\(p\) 为质数。

考虑 \(m=p\) 怎么做。

\(n=0\),那么只要 \(\exist i\in [1,k]=0\) ,用总方案减去非零方案即可,\(m^k-(m-1)^k\)

否则,考虑前 \(k-1\) 个任意选,最后一个数填 \(\frac{n}{\prod\limits_{i=1}^{k-1} a_i}\) 即可,方案数 \((m-1)^{k-1}\)

考虑 \(m=p^c\) 怎么做。

\(n\neq 0\),设 \(n=tp^x\),先把 \(p^x\) 处理掉,就是把这 \(x\)\(p\) 分为 \(k\) 组,每组额外乘上一个数。

显然一组可能没有或有多个 \(p\),方案数 \(\binom{x+k-1}{k-1}\)\(k-1\) 太大,转化为求 \(\binom{x+k-1}{x}\),显然这个 \(x\)\(\log\) 级别的。

对于剩下的,其实就是上面的情况,只不过不能填 \(p\) 的倍数了。

方案数:\(\binom{x+k-1}{x}(m-\frac{m}{p})^{k-1}\)

\(n=0\),还是考虑用总方案数减去非零方案数,枚举 \(x\),那么 \(t\) 可以为 \([1,p^{c-x}]\) 中任意数,注意减去 \(p\) 的倍数个数,\(\frac{p^{c-x}}{p}=p^{c-x-1}\)

方案数:\(m^k-\sum\limits_{0\leq x <c} (p^{c-x}-p^{c-x-1})\binom{x+k-1}{x}(m-\frac{m}{p})^{k-1}\)

考虑一般情况怎么做。

根据算数基本定理,\(m=\prod p_i^{c_i}\)

对于每个 \(p_i^{c_i}\),计算 \(n\bmod {p_i^{c_i}}\) 时的答案,幷乘起来即为答案。

其实这有点像 CRT,感性理解一下。

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int mod=998244353;
int k,n,m,ans=1,inv[60];
int qpow(int a,int b) {int r=1;for(a%=mod;b;b>>=1,a=a*a%mod) if(b&1) r=r*a%mod;return r;}

int C(int n,int m)
{
    int res=1;
    for(int i=1;i<=m;i++) res=res*(n-m+i)%mod*inv[i]%mod;
    return res;
}

int calc(int n,int m,int p,int c)
{
    int pw=qpow(m-m/p,k-1);
    if(n!=0)
    {
        int x=0;
        while(n%p==0) n/=p,x++;
        return C(x+k-1,x)*pw%mod;
    }
    else
    {
        int res=qpow(m,k);
		for(int i=0;i<c;i++) res=(res-(qpow(p,c-i)-qpow(p,c-i-1)+mod)%mod*C(i+k-1,i)%mod*pw%mod+mod)%mod;
		return res;
    }
}

signed main()
{
    cin>>k>>n>>m;
    inv[1]=1;for(int i=2;i<=50;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
    for(int i=2;i*i<=m;i++)
        if(m%i==0)
        {
            int _m=1,c=0;
            while(m%i==0) _m*=i,c++,m/=i;
            ans=ans*calc(n%_m,_m,i,c)%mod;
        }
    if(m>1) ans=ans*calc(n%m,m,m,1)%mod;
    cout<<ans<<'\n';
}

B1019 T4 非攻

csp-s rp++!

推推你的式子 /yim !(推式子能力还是太菜了啊)

说实话 50 pts 很好拿。

考虑把一个排列分为几个置换环,就是 \(p_i\)\(i\) 连边形成的一些环。

比如 \(3,1,4,2,5\) 可以分为 \((3,1,4,2),(5)\)

设环的大小为 \(sz\),那么需要交换 \(sz-1\) 次。

贪心的,每次用环内最小值交换,最小代价为最小值乘上剩余元素之和。

考虑枚举 \(1 \sim n-1\) 每个数作为环内最小值时的贡献,则:

\(\begin{aligned} ans&=\sum\limits_{i=1}^{n-1}\sum\limits_{S\subset (i,n]} i (\sum\limits_{x\in S}x) |S|! (n-|S|-1)!\\ &=\sum\limits_{i=1}^{n-1} i \frac{(n+i+1)(n-i)}{2} \sum\limits_{s=1}^{n-i}\binom{n-i-1}{s-1} s! (n-s-1)! \ \ \ \ \ \ (1)\\ &=\frac{1}{2} \sum\limits_{i=1}^{n-1} i (n+i+1)(n-i)!\sum\limits_{s=1}^{n-i} \frac{s (n-s-1)!}{(n-s-i)!}\\ \end{aligned}\)

推到 (1) 就能拿 50 pts 了,赞美良心出题人。

稍微说一下 (1),使用笨蛋列举法。

假设 \(i=1,n=4\),枚举当前集合大小 \(s\)

\(s=1\),那么所有集合情况为:\(\{2\},\{3\},\{4\}\)

\(s=2\),那么所有集合情况为:\(\{2,3 \},\{2,4 \},\{3,4 \}\)

\(s=3\),那么所有集合情况为:\(\{2,3,4\}\)

可以发现每个元素出现次数是一样的,于是只用计算一个元素的出现次数。

集合有 \(\binom{n-i}{s}\) 种可能,钦定一个元素必须选,那么有 \(\binom{n-i-1}{s-1}\) 种可能。

继续推后面那一坨。

\(\begin{aligned} \sum\limits_{s=1}^{n-i} \frac{s (n-s-1)!}{(n-s-i)!}&= (i-1)!\sum\limits_{s=1}^{n-i} s \binom{n-s-1}{i-1}\\ &=(i-1)!\sum\limits_{s=2}^{n-i+1}(s-1)\binom{n-s}{i-1}\\ &=(i-1)!\sum\limits_{s=i-1}^{n-2}(n-s-1)\binom{s}{i-1}\\ &=(i-1)!n \sum\limits_{s=i-1}^{n-2}\binom{s}{i-1} + (i-1)! \sum\limits_{s=i-1}^{n-2} (s+1) \binom{s}{i-1}\\ &=(i-1)!n\sum\limits_{s=i-1}^{n-2}\binom{s}{i-1}+(i-1)! i \sum\limits_{s=i-1}^{n-2}\binom{s+1}{i}\\ &=(i-1)!n\sum\limits_{s=i-1}^{n-2}\binom{s}{i-1}+i!\sum\limits_{s=i}^{n-1}\binom{s}{i}\\ &=(i-1)!n\binom{n-1}{i}-i!\binom{n}{i+1} \end{aligned}\)

说一下倒数第二行的 \(\sum\limits_{s=i}^{n-1}\binom{s}{i}=\binom{n}{i+1}\)

考虑在 \(n\) 个数中选 \(i+1\) 个,枚举最后一个选的数为 \(s+1\),那么剩余 \(i\) 个可以在 \(1\sim s\) 中随便选。

带回原式就可以 \(O(n)\) 计算了,但这还不够优雅。

\(\begin{aligned} ans&=\frac{1}{2} \sum\limits_{i=1}^{n-1} i (n+i+1)(n-i)!(i-1)!n\binom{n-1}{i}-i (n+i+1)(n-i)!i!\binom{n}{i+1}\\ &=\frac{1}{2}\sum\limits_{i=1}^{n-1} (n+i+1)(n-i)n!-\frac{i(n+i+1)(n-i)n!}{i+1}\\ &=\frac{1}{2}\sum\limits_{i=1}^{n-1} \frac{(n+i+1)(n-i)n!}{i+1} \end{aligned}\)

题解说还有基于分治 NTT &@*¥%¥&,可以进一步优化到 \(O(\sqrt n \log n)\),我肯定不会。

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=1e7+5,p=1e9+7;
int n,ans,fac,inv[N];

signed main()
{
    cin>>n;
    inv[1]=1;for(int i=2;i<=n;i++) inv[i]=1ll*(p-p/i)*inv[p%i]%p;
    fac=1   ;for(int i=1;i<=n;i++) fac=fac*i%p;
    for(int i=1;i<n;i++) ans=(ans+1ll*(n+i+1)%p*(n-i)%p*fac%p*inv[i+1]%p)%p;
    cout<<ans*500000004%p;
}

B1023 T3 彩排

神仙构造题。

不妨倒着构造,那么题意可转化为:

给你一个 \(1\sim n\) 排列 \(a\),让你按一种方法构造一个排列 \(b\)\(b_{1\sim n}=a_{1\sim n}\),且 \(b\) 最后 \(n\) 个元素为 \(n,n-1,...,1\)

怎么个方法呢?大概是这样:

一开始 \(X=a_1\),假设当前位置为 \(i,i>n\),那么有两种选择:

\(\begin{cases} b_i=b_{n-i+1} \\ b_i=X,X=b_i \end{cases}\)

除去 \(a_1\),让每 \(n-1\) 一组,每一组继承上一组的 \(a\) 数组,并在此基础上进行操作,那么目标就是让 \(a_{2\sim n}=n,n-1,...,2\),最后把 \(X=1\) 放最后。

考虑遍历当前组,假设当前位置在 \(a\) 数组的下标为 \(i\),那么有两种情况:

  • \(X=n-i+2\),即 \(X\) 的值是当前位置的目标,直接交换 \(a_i\)\(X\) 的值。
  • \(X=1\),找到一个 \(j\)\(a_{j}\neq n-j+2\),交换 \(a_j\)\(X\) 的值。

第一种很好理解,而第二种你让 \(a_{j}=X\),那么在下一组就可以把 \(a_j\) 变为目标。

注意 \(1\) 不是任何 \(a_{2\sim n}\) 的目标,所以最后 \(X=1\)

发现对于一个大小为 \(sz\) 的置换环,会进行 \(sz-1\) 次操作,而在随意数据下置换环会很多,所以长度大概是在 \(\frac{n^2}{2}\) 级别,具体我也不会证。

#include<bits/stdc++.h>
using namespace std;

const int N=1005,M=6e5+5;
int n,m,x,a[N],b[M];
bool chk() {for(int i=2;i<=n;i++) if(a[i]!=n-i+2) return 0;return 1;}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    reverse(a+1,a+1+n);
    x=a[1];
    for(int i=1;i<=n;i++) b[++m]=a[i];
    while(!chk())
    {
        for(int i=2;i<=n;i++)
        {
            if(x==n-i+2||(x==1&&a[i]!=n-i+2)) swap(x,a[i]);
            b[++m]=a[i];
        }
    }
    b[++m]=1;
    reverse(b+1,b+1+m);
    cout<<m<<'\n';
    for(int i=1;i<=m;i++) cout<<b[i]<<' ';
}

B1026 T4 挖矿

不知道 cqbz 给的这两套 noip 模拟赛题是什么 jb。

不过这是道很好的数据结构题,虽然好像是原。

假设一个区间合法,那么 \(mx-mn+1=sz\),其中 \(mx,mn\) 是区间最大/小值,\(sz\) 是区间大小。

然后可以 ST 表 + 扫描线 \(O(n^3)\) 做。

不过正解和这个没有关系。

考虑只涂黑权值在 \([l,r]\) 内的位置,其余位置留白,看所有黑色的位置是否构成一个矩形。

厉害的来了,考虑所有 \((n+1)\cdot (m+1)\)\(2\times 2\) 小正方形(超出边界的也算),那么构成矩形当且仅当有 \(4\) 个小正方形内部有 \(1\) 个黑色格子,没有 \(1\) 个小正方形内部有 \(3\) 个黑色格子。

正确性显然,画画图就知道了。

从小到大枚举 \(r\),对于每个 \(l \leq r\),维护 \(f(l)\) 表示染黑权值为 \([l,r]\) 区间内的位置后,有多少小正方形内部有 \(1\) 个或 \(3\) 个黑色格子。

显然有 \(f(l) \geq 4,f(r)=4\),直接线段树维护区间 \(f(i)\) 最小值,区间 \(f(i)=4\) 的个数即可。

对于每次增加一个 \(r\),显然只会影响到 \(4\) 个 小正方形,分别计算贡献即可。

好像有更智慧的写法,可惜我不智慧。

#include<bits/stdc++.h>
using namespace std;

const int N=2e5+5;
int n,m,V;
long long ans;
pair<int,int> pos[N];
vector<vector<int>> a;

#define lc (k<<1)
#define rc (k<<1|1)
#define mid (l+r>>1)
int mn[N<<2],num[N<<2],add[N<<2];
void pushup(int k)
{
    mn[k]=min(mn[lc],mn[rc]);
    num[k]=0;
    if(mn[lc]==mn[k]) num[k]+=num[lc];
    if(mn[rc]==mn[k]) num[k]+=num[rc];
}
void addtag(int k,int v) {mn[k]+=v,add[k]+=v;}
void pushdown(int k)
{
    if(!add[k]) return;
    addtag(lc,add[k]),addtag(rc,add[k]);
    add[k]=0;
}
void upd(int x,int y,int v,int k=1,int l=1,int r=V)
{
    if(l>r) return;
    if(l>=x&&r<=y) {addtag(k,v);return;}
    pushdown(k);
    if(x<=mid) upd(x,y,v,lc,l,mid);
    if(mid<y) upd(x,y,v,rc,mid+1,r);
    pushup(k);
}
void point_init(int x,int k=1,int l=1,int r=V)
{
    if(l==r) {mn[k]=4;num[k]=1;return;}
    x<=mid?point_init(x,lc,l,mid):point_init(x,rc,mid+1,r);
    pushup(k);
}

void work(int a,int b,int c,int d)
{
    if(!b) b=1e9;if(!c) c=1e9;if(!d) d=1e9;
    if(c<b) swap(c,b);if(d<b) swap(b,d);if(d<c) swap(c,d);
    //r=a 其余四个值从小到大排序
    int cnt=0;
    if(d<a) {upd(d+1,a-1,(cnt==1||cnt==3)?-1:1);cnt++;}
    if(c<a) {upd(c+1,min(a-1,d),(cnt==1||cnt==3)?-1:1);cnt++;}
    if(b<a) {upd(b+1,min(a-1,c),(cnt==1||cnt==3)?-1:1);cnt++;}
    if(b<a) upd(1,b,(cnt==1||cnt==3)?-1:1);
    if(b>a&&a!=1) upd(1,a-1,1);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;V=n*m;
    a.resize(n+2,vector<int>(m+2));
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) cin>>a[i][j],pos[a[i][j]]={i,j};
    memset(mn,0x3f,sizeof(mn));
    for(int i=1;i<=V;i++)
    {
        point_init(i);
        auto [x,y]=pos[i];
        work(i,a[x+1][y],a[x][y+1],a[x+1][y+1]);
        work(i,a[x-1][y],a[x][y+1],a[x-1][y+1]);
        work(i,a[x+1][y],a[x][y-1],a[x+1][y-1]);
        work(i,a[x-1][y],a[x][y-1],a[x-1][y-1]);
        ans+=num[1];
    }
    cout<<ans<<'\n';
}
posted @ 2023-10-04 20:57  spider_oyster  阅读(11)  评论(0编辑  收藏  举报