10月杂题题解

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

CF814E An unavoidable detour for home

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

Part 1

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

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

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

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

Part 2

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

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

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

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

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

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

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

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

即:(x+2y)!(x+2y2z)!×2z×z!

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

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

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

y 个度数为 2 的点选出这些边的方案:y!(ys)!×p!×q!

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

那么现在选了 s 条边,2s 个点(因为拆点了),还剩 x+2y2s 个点,zs 条边。

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

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

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

s=2p+q,smin(y,z)(1)p+q×y!(ys)!×p!×q!×(x+2y2s)!(x2y2z)!×2zs×(zs)!×(x+2y2z)!×12y

Part 3

ts=s=2p+q(1)p+qp!×q!

则原式可以写为:

0smin(y,z)ts×y!×(x+2y2s)!(ys)!×2y×1(zs)!×2zs

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

那么可以枚举 z,s 转移到 f(i+j,x+2y2z)。但是这样是 O(n4) 的。

注意上面那个式子,前面只需要枚举 s,后面枚举 zs 就能转移。

那么记一个辅助数组 g(i,j),先枚举 sf(i,j) 转移到 g(i+j,x+2y2s),再枚举 zs 转移到 f(i+j,x+2y2z)

时间复杂度 O(n3),空间复杂度 O(n2)

#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,使 bl1,br+1,代价是 (rl)2

最少天数显然是 max(bi,0)

对于一个位置 bi<0,寻找前面的 bl,并操作使得 bi=0

由于是差分数组,显然每个 bi<0 都能找到对应的一些 bl,但最后会剩一些 bibi>0

这时只需要令 r=n+1,br=inf 即可,然后消去这些 bi>0

剩下的 bi 的和显然是不变的。

对于 bi>0,肯定是留在最后消代价最大。

那么应该让消去剩下这些 bi 的代价最大/小。

这里考虑令代价最大的情况,那每次对于 bi<0 选择离其最近的 bl,也就是尽可能留住那些离 n+1 远的 bi>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

那么转移合法当且仅当:

{ST=iu=k+1iv=k+1iuk+1xT<iu

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

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

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

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

枚举当前对应的操作是 j

  • 如果是删除子树,那么 iu=k+1xS<j
  • 如果是保留子树,那么 j<iu

当然还有最基本的子树大小限制。考虑对于 si,实际上是删除了 si1si 大小的子树。

那么枚举 kS 就可以算出当前子树的大小了。

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

#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 个。pi...n 中有 s 个旧的,y 之后有 k 个旧的,m 个新的,那么:

cx+sk=cy+k+m

cxcy+s=2k+m

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

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

即若能凑出 t,那么也能凑出 t2

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

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

最后如果 cxcy, 则无解。

#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] 之间随机变量 x1,x2...,xn,第 k 小的那个期望值是 k+1n

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

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

总共可以插入 n 个位置,那么概率为 k+1n

Part 2

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

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

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

也就是说加入前 i1 条边图都是不连通的。

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

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

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

1m+11imiP(i)

等价于:

1m+11imijmP(j)

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

发现 ijmP(j) 就是加入前 i1 条边图不连通的概率。

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

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

考虑如何转移。

可以枚举一个连通的子集 T,得到另一个子集 S=SxorT,规定 TS 之间不连边,然后 S 内可以任意连。

但如果存在一对 TiSj,就会算重。

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

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

Part 3

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

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

显然有 f(S,i,0)+f(S,i,1)=CszSi,不难列出转移方程:

f(S,i+j,0)=f(T,i,0)CszSj

f(S,i,1)=CszSif(S,i,0)

答案为

1m+1f(2n1,i,0)Cmi

#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) 表示 in 的期望天数。

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

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

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

g(i)=jf(j)<f(i)1pi,j

那么有:

f(i)=jf(j)<f(i)f(j)×pi,j×g(j)+f(i)×g(i)+1

移项得:

f(i)=jf(j)<f(i)f(j)×pi,j×g(j)+11g(i)

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

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

细节上由于 1g(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,那么只要 \existi[1,k]=0 ,用总方案减去非零方案即可,mk(m1)k

否则,考虑前 k1 个任意选,最后一个数填 ni=1k1ai 即可,方案数 (m1)k1

考虑 m=pc 怎么做。

n0,设 n=tpx,先把 px 处理掉,就是把这 xp 分为 k 组,每组额外乘上一个数。

显然一组可能没有或有多个 p,方案数 (x+k1k1)k1 太大,转化为求 (x+k1x),显然这个 xlog 级别的。

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

方案数:(x+k1x)(mmp)k1

n=0,还是考虑用总方案数减去非零方案数,枚举 x,那么 t 可以为 [1,pcx] 中任意数,注意减去 p 的倍数个数,pcxp=pcx1

方案数:mk0x<c(pcxpcx1)(x+k1x)(mmp)k1

考虑一般情况怎么做。

根据算数基本定理,m=pici

对于每个 pici,计算 nmodpici 时的答案,幷乘起来即为答案。

其实这有点像 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 很好拿。

考虑把一个排列分为几个置换环,就是 pii 连边形成的一些环。

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

设环的大小为 sz,那么需要交换 sz1 次。

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

考虑枚举 1n1 每个数作为环内最小值时的贡献,则:

ans=i=1n1S(i,n]i(xSx)|S|!(n|S|1)!=i=1n1i(n+i+1)(ni)2s=1ni(ni1s1)s!(ns1)!      (1)=12i=1n1i(n+i+1)(ni)!s=1nis(ns1)!(nsi)!

推到 (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}

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

集合有 (nis) 种可能,钦定一个元素必须选,那么有 (ni1s1) 种可能。

继续推后面那一坨。

s=1nis(ns1)!(nsi)!=(i1)!s=1nis(ns1i1)=(i1)!s=2ni+1(s1)(nsi1)=(i1)!s=i1n2(ns1)(si1)=(i1)!ns=i1n2(si1)+(i1)!s=i1n2(s+1)(si1)=(i1)!ns=i1n2(si1)+(i1)!is=i1n2(s+1i)=(i1)!ns=i1n2(si1)+i!s=in1(si)=(i1)!n(n1i)i!(ni+1)

说一下倒数第二行的 s=in1(si)=(ni+1)

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

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

ans=12i=1n1i(n+i+1)(ni)!(i1)!n(n1i)i(n+i+1)(ni)!i!(ni+1)=12i=1n1(n+i+1)(ni)n!i(n+i+1)(ni)n!i+1=12i=1n1(n+i+1)(ni)n!i+1

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

#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 彩排

神仙构造题。

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

给你一个 1n 排列 a,让你按一种方法构造一个排列 bb1n=a1n,且 b 最后 n 个元素为 n,n1,...,1

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

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

{bi=bni+1bi=X,X=bi

除去 a1,让每 n1 一组,每一组继承上一组的 a 数组,并在此基础上进行操作,那么目标就是让 a2n=n,n1,...,2,最后把 X=1 放最后。

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

  • X=ni+2,即 X 的值是当前位置的目标,直接交换 aiX 的值。
  • X=1,找到一个 jajnj+2,交换 ajX 的值。

第一种很好理解,而第二种你让 aj=X,那么在下一组就可以把 aj 变为目标。

注意 1 不是任何 a2n 的目标,所以最后 X=1

发现对于一个大小为 sz 的置换环,会进行 sz1 次操作,而在随意数据下置换环会很多,所以长度大概是在 n22 级别,具体我也不会证。

#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。

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

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

然后可以 ST 表 + 扫描线 O(n3) 做。

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

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

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

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

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

显然有 f(l)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';
}

本文作者:spider_oyster

本文链接:https://www.cnblogs.com/spider-oyster/p/17742742.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   spider_oyster  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起