普及模拟3

普及模拟3

\(T1\) 最大生成树 \(100pts\)

  • 简化题意:给定一个 \(n(1 \le n \le 1 \times 10^5)\) 个点的完全图,给定各点的点权 \(a_i(1 \le i \le n)\) ,两点间的边权为 \(|a_i-a_j|\) ,求该图的最大生成树。
  • 正解:贪心,考虑到一个点对答案产生的贡献为 \(\max(a_i-\min\limits_{j=1}^{n} \{ a_j \},\max\limits_{j=1}^{n} \{ a_j \}-a_i)\) ,又因为是完全图,易证得连边后一定是符合题意的解。
    ll a[100001];//十年OI一场空,不开long long见祖宗
    int main()
    {
        ll n,i,ans=0;
        cin>>n;
        for(i=1;i<=n;i++)
        {
            cin>>a[i];
        }
        sort(a+1,a+1+n);
        for(i=1;i<=n-1;i++)//只需要n-1条边
        {
            ans+=max(a[n]-a[i],a[i]-a[1]);
        }
        cout<<ans<<endl;
        return 0;
    }
    
  • 貌似是首A。

\(T2\) 红黑树 \(80pts\)

  • 简化题意:给定一棵 \(n(1 \le n \le 1 \times 10^5)\) 个点的有根树, \(1\) 为根节点,初始每个点都是红色,有 \(n\) 次操作,每次操作会把 \(p_i\) 变成黑色,保证操作序列 \(p\) 是一个排列,即每次操作的点都不相同。每次操作完成后,你需要输出有多少个子树是红黑子树,即子树内既包含黑点又包含红点。
    • 本题中的红黑树与 红黑树 无任何关系。
  • 部分分:
    • \(20pts\) :对链的情况特判。
    • \(80pts\)(在线做法) :一遍 \(DFS\) 处理出以 \(i(1 \le i \le n)\) 为根的子树大小,每次修改暴力跳父亲直到根节点,卡在了链的测试点上,复杂度为 \(O(n^2)\)
      struct node
      {
      	int nxt,to;
      }e[200001];
      int head[200001],siz[200001],fa[200001],vis[200001],cnt=0;
      void add(int u,int v)
      {
      	cnt++;
      	e[cnt].nxt=head[u];
      	e[cnt].to=v;
      	head[u]=cnt;
      }
      void dfs(int x,int father)
      {
      	fa[x]=father;
      	for(int i=head[x];i!=0;i=e[i].nxt)
      	{
      		if(e[i].to!=father)
      		{
      			dfs(e[i].to,x);
      			siz[x]+=siz[e[i].to]+1;
      		}
      	}
      }
      int main()
      {
      	int n,i,j,u,v,p,ans=0;
      	cin>>n;
      	for(i=1;i<=n-1;i++)
      	{
      		cin>>u;
      		v=i+1;
      		add(u,v);
      	}
      	dfs(1,0);
      	for(i=1;i<=n;i++)
      	{
      		siz[i]++;
      	}
      	for(i=1;i<=n;i++)
      	{
      		cin>>p;
      		for(j=p;j!=0;j=fa[j])
      		{
      			siz[j]--;
      			if(vis[j]==0)
      			{
      				if(1<=siz[j])
      				{
      					vis[j]=1;
      					ans++;
      				}
      			}
      			else
      			{
      				if(!(1<=siz[j]))
      				{
      					vis[j]=0;
      					ans--;
      				}
      			}
      		}
      		cout<<ans<<" ";
      	}
      	return 0;
      }
      
  • 正解(离线做法):
    • 前置知识:红黑子树的产生与消失都是由改变颜色的这个点造成的。
      • 例如,把节点 \(x\) 变成黑色后,对于所有包含 \(x\) 的子树 \(T\) ,如果 \(x\) 是该子树内第一个变成黑色的点,那么 \(x\) 会让 \(T\) 变成红黑子树;如果 \(x\) 是该子树内最后一个红色的点,那么 \(x\) 会让 \(T\) 变成非红黑子树。
    • 记录下这棵子树中黑点最先出现和最后一个红点变成黑点的时候。
      • 做法一:暴力跳父亲时跳到某个点时,若这个点既不是以这个点的父亲节点为根的子树内最先出现的黑点,也不是最后一个变成黑点的红点时就没有必要跳了,因为不会对答案产生贡献了(这里可能有些绕口,自己可以画图模拟一下)。
        • 复杂度貌似很神奇,官方题解写的是 \(O(n)\)
          struct node
          {
              int nxt,to;
          }e[200001];
          int head[200001],minn[200001],maxx[200001],p[200001],id[200001],fa[200001],cnt=0;//minn表示(····)最早出现,maxx表示最后一个(····)消失
          void add(int u,int v)
          {
              cnt++;
              e[cnt].nxt=head[u];
              e[cnt].to=v;
              head[u]=cnt;
          }
          void dfs(int x)
          {
              minn[x]=maxx[x]=id[x];
              for(int i=head[x];i!=0;i=e[i].nxt)
              {
                  dfs(e[i].to);
                  minn[x]=min(minn[x],minn[e[i].to]);
                  maxx[x]=max(maxx[x],maxx[e[i].to]);
              }
          }
          int main()
          {
              int n,i,j,u,v,ans=0,flag;
              cin>>n;
              for(i=1;i<=n-1;i++)
              {
                  cin>>u;
                  v=i+1;
                  fa[v]=u;
                  add(u,v);
              }
              for(i=1;i<=n;i++)
              {
                  cin>>p[i];
                  id[p[i]]=i;
              }
              dfs(1);
              for(i=1;i<=n;i++)
              {
                  for(j=p[i];j!=0;j=fa[j])//暴力跳父亲
                  {
                      flag=0;
                      if(minn[j]==id[p[i]])
                      {
                          ans++;
                          flag=1;
                      }
                      if(maxx[j]==id[p[i]])
                      {
                          ans--;
                          flag=1;
                      }
                      if(flag==0)
                      {
                          break;
                      }
                  }
                  cout<<ans<<" ";
              }
              return 0;
          }
          
      • 做法二:进行差分维护一下。————隔壁 @xrlong 的做法
        • 树状数组的区间修改单点查询操作
          struct node
          {
              int nxt,to;
          }e[200001];
          int head[200001],minn[200001],maxx[200001],p[200001],id[200001],c[400001],cnt=0;
          int lowbit(int x)
          {
              return (x&(-x));
          }
          void add(int u,int v)
          {
              cnt++;
              e[cnt].nxt=head[u];
              e[cnt].to=v;
              head[u]=cnt;
          }
          void dfs(int x)
          {
              minn[x]=maxx[x]=id[x];
              for(int i=head[x];i!=0;i=e[i].nxt)
              {
                  dfs(e[i].to);
                  minn[x]=min(minn[x],minn[e[i].to]);
                  maxx[x]=max(maxx[x],maxx[e[i].to]);
              }
          }
          void update(int n,int x,int key)
          {
              for(int i=x;i<=n;i+=lowbit(i))
              {
                  c[i]+=key;
              }
          }
          int getsum(int x)
          {
              int ans=0;
              for(int i=x;i>0;i-=lowbit(i))
              {
                  ans+=c[i];
              }
              return ans;
          }
          int main()
          {
              int n,i,j,u,v,ans=0,flag;
              cin>>n;
              for(i=1;i<=n-1;i++)
              {
                  cin>>u;
                  v=i+1;
                  add(u,v);
              }
              for(i=1;i<=n;i++)
              {
                  cin>>p[i];
                  id[p[i]]=i;
              }
              dfs(1);
              for(i=1;i<=n;i++)
              {
                  update(n,minn[i],1);
                  update(n,maxx[i]-1+1,-1);
              }
              for(i=1;i<=n;i++)
              {
                  cout<<getsum(i)<<" ";
              }
              return 0;
          }
          
        • 差分
          struct node
          {
              int nxt,to;
          }e[200001];
          int head[200001],minn[200001],maxx[200001],p[200001],id[200001],sum[400001],cnt=0;
          void add(int u,int v)
          {
              cnt++;
              e[cnt].nxt=head[u];
              e[cnt].to=v;
              head[u]=cnt;
          }
          void dfs(int x)
          {
              minn[x]=maxx[x]=id[x];
              for(int i=head[x];i!=0;i=e[i].nxt)
              {
                  dfs(e[i].to);
                  minn[x]=min(minn[x],minn[e[i].to]);
                  maxx[x]=max(maxx[x],maxx[e[i].to]);
              }
          }
          int main()
          {
              int n,i,j,u,v,ans=0;
              cin>>n;
              for(i=1;i<=n-1;i++)
              {
                  cin>>u;
                  v=i+1;
                  add(u,v);
              }
              for(i=1;i<=n;i++)
              {
                  cin>>p[i];
                  id[p[i]]=i;
              }
              dfs(1);
              for(i=1;i<=n;i++)
              {
                  sum[minn[i]]++;
                  sum[maxx[i]-1+1]--;
              }
              for(i=1;i<=n-1;i++)
              {
                  ans+=sum[i];
                  cout<<ans<<" ";
              }
              cout<<"0";
              return 0;
          }
          

\(T3\) 校门外的树 \(30pts\)

  • 简化题意:门外有 \(n\) 棵树,每棵树有一个初始高度 \(h_i\) 和生长速度 \(v_i\) ,在接下来的 \(m\) 天中,第 \(i\) 棵树会长高 \(v_i\) ,之后有两种事件可能会发生,操作如下(对 \(2^{64}\) 取模):
    • 操作一:将 \([l,r]\) 内树的生长速度 \(v_i(l \le i \le r)\) 增加 \(v\)
    • 操作二:求 \(\sum\limits_{i=l}^{r} h_i\)
  • 部分分:
    • \(30pts\)\(n^2\) 暴力枚举。
    • \((30+20=50)pts\) :考虑到没有修改操作,维护一下即可。
  • 正解:
    • 简化版:若一个数 \(a\) ,有它的增长量 \(v\) ,在第 \(i\) 次操作时,有 \(a_{i}=a_{i-1}+v=a_1+i \times v\)

    • \(2^{64}\) 取模直接用 \(unsigned \ long \ long\) 自然溢出即可。

    • \(a_{i,j}\) 表示第 \(i\) 棵树第\(j\) 天的高度,特别地,有 \(a_{i,0}=h_i\) ,接着进行前缀和优化 \(sum_i=\sum\limits_{j=1}^{i} a_{j,0}(1 \le i,j \le n)\)

    • 将操作二式子进行化简,有

      \(\begin{aligned}\sum\limits_{i=l}^{r} a_{i,k} \end{aligned}\)

      \(\begin{aligned} &=\sum\limits_{i=l}^{r} a_{i,0} \times k \times \sum\limits_{i=l}^{r} v_{i,num_i}-\sum\limits_{i=l}^{r}\sum\limits_{j=1}^{num_i}v_{i,j} \times day_{i,j} \end{aligned}\)

      \(\begin{aligned} &= sum[r]-sum[l-1]+k \times \sum\limits_{i=l}^{r} v_{i,num_i}-\sum\limits_{i=l}^{r}\sum\limits_{j=1}^{num_i-1}(v_{i,j+1}-v_{i,j}) \times day_{i,j}\end{aligned}\)

      其中 \(day_{i,j}\) 表示第 \(i\) 棵树第 \(j\) 次修改速度时是第几天,\(num_i\) 表示第 \(i\) 棵树生长速度修改的次数, \(k\) 表示本次是第 \(k\) 次操作。

    • 考虑用线段树维护 \(\sum\limits_{i=l}^{r} v_{i,num_i}\)\(\sum\limits_{i=l}^{r}\sum\limits_{j=1}^{num_i-1}(v_{i,j+1}-v_{i,j}) \times day_{i,j}\) ,将其 分别储存在两棵线段树 或存在一棵线段树中(其实差别不大)。

    ull h[3000000],v[3000000],sum[3000000];
    struct SegmentTree
    {
        ull l,r,sum[2],lazy[2];
    }tree[3000000];
    ull lson(ull x)
    {
        return x*2;
    }
    ull rson(ull x)
    {
        return x*2+1;
    }
    void pushup(ull rt,ull pd)
    {
        tree[rt].sum[pd]=tree[lson(rt)].sum[pd]+tree[rson(rt)].sum[pd];
    }
    void build(ull rt,ull l,ull r,ull pd)
    {
        tree[rt].l=l;
        tree[rt].r=r;
        tree[rt].lazy[pd]=0;
        if(l==r)
        {
            tree[rt].sum[pd]=v[l];
            return;
        }
        ull mid=(l+r)/2;
        build(lson(rt),l,mid,pd);
        build(rson(rt),mid+1,r,pd);
        pushup(rt,pd);
    }
    void pushdown(ull rt,ull pd)
    {
        if(tree[rt].lazy[pd]!=0)
        {
            tree[lson(rt)].lazy[pd]+=tree[rt].lazy[pd];
            tree[rson(rt)].lazy[pd]+=tree[rt].lazy[pd];
            tree[lson(rt)].sum[pd]+=tree[rt].lazy[pd]*(tree[lson(rt)].r-tree[lson(rt)].l+1);
            tree[rson(rt)].sum[pd]+=tree[rt].lazy[pd]*(tree[rson(rt)].r-tree[rson(rt)].l+1);
            tree[rt].lazy[pd]=0;
        }
    }
    void update(ull rt,ull l,ull r,ull val,ull pd)
    {
        if(l<=tree[rt].r&&tree[rt].l<=r)
        {
            if(l<=tree[rt].l&&tree[rt].r<=r)
            {
                tree[rt].lazy[pd]+=val;
                tree[rt].sum[pd]+=val*(tree[rt].r-tree[rt].l+1);
                return;
            }
            pushdown(rt,pd);
            update(lson(rt),l,r,val,pd);
            update(rson(rt),l,r,val,pd);
            pushup(rt,pd);
        }
    }
    ull query(ull rt,ull l,ull r,ull pd)
    {
        if(r<tree[rt].l||tree[rt].r<l)
        {
            return 0;
        }
        if(l<=tree[rt].l&&tree[rt].r<=r)
        {
            return tree[rt].sum[pd];
        }
        pushdown(rt,pd);
        return query(lson(rt),l,r,pd)+query(rson(rt),l,r,pd);
    }
    int main()
    {
        ull n,m,i,pd,l,r,v2;
        cin>>n>>m;
        for(i=1;i<=n;i++)
        {
            cin>>h[i]>>v[i];
            sum[i]=sum[i-1]+h[i];
        }
        build(1,1,n,0);
        for(i=1;i<=m;i++)
        {
            cin>>pd>>l>>r;
            if(pd==1)
            {
                cin>>v2;
                update(1,l,r,v2,0);
                update(1,l,r,v2*i,1);
            }
            if(pd==2)
            {
                cout<<(sum[r]-sum[l-1])+i*query(1,l,r,0)-query(1,l,r,1)<<endl;
            }
        }
        return 0;
    }
    
    • 隔壁有口胡李超线段树的做法,不是很懂。

    • 还有口胡吉司机线段树的。

\(T4\) 种树 \(0pts\)

  • 简化题意:约翰要带 \(n\) 只牛去参加 \(q\) 次集会里的展示活动(每次集会是相互独立的),这些牛可以是公牛,也可以是母牛。牛们要站成一排,但是公牛是好斗的,为了避免公牛闹出乱子,约翰决定第 \(i(1 \le i \le q)\) 次集会中第 \(x_i\) 头必须是母牛,且任意两只公牛之间至少要有 \(k_i-1\) 只母牛。请计算一共有多少种排队的方法,所有公牛可以看成是相同的,所有母牛也一样。答案对 \(998244353\) 取模。
  • 弱化版:luogu P6191 Bulls And Cows S
  • 部分分:
    • \(0pts\) :当 \(k=1\) 时,易知方案数为 \(2^{n-1}\)
    • \(60pts\) :计数类型的 \(DP\)
      • 对于第 \(j(1 \le j \le q)\) 次集会,令 \(f_i(0 \le i \le n)\) 表示第 \(i\) 头是母牛的方案数, \(g_i(0 \le i \le n)\) 表示第 \(i\) 头是公牛的方案数。由定义,有 \(f_0=g_0=0,f_1=1,g_1=\begin{cases}1 & x \ne 1 \\ 0 & x=1\end{cases}\) 。容易对于 \(2 \le i \le n\) ,有递推式 \(f_i=f_{i-1}+g_{i-1},g_i=\begin{cases}0 & x=i \\ 1 & x \ne i,i \le k \\ f_{i-k}+g_{i-k} & x \ne i,i>k \end{cases}\)
      • \(f_n+g_n\) 即为所求。
      • 单次时间复杂度为 \(O(n)\)
        • \(hack\) 数据生成器如下。
          int main()
          {
              srand(time(0));
              cout<<100000<<" "<<100000<<endl;
              for(int i=1;i<=100000;i++)
              {
                  cout<<(rand()%100000+1)<<" "<<i<<endl;
              }
          }
          
        ll f[100001],g[100001];
        int main()
        {
        	ll n,q,i,j,x,k;
        	cin>>n>>q;
        	for(i=1;i<=q;i++)
        	{
        		cin>>x>>k;
        		f[1]=1;
        		if(x!=1)
        		{
        			g[1]=1;
        		}
        		else
        		{
        			g[1]=0;
        		}
        		for(j=2;j<=n;j++)
        		{
        			f[j]=(f[j-1]+g[j-1])%998244353;
        			if(j==x)
        			{
        				g[j]=0;
        			}
        			else
        			{
        				if(j<=k)
        				{
        					g[j]=1;
        				}
        				else
        				{
        					g[j]=(f[j-k]+g[j-k])%998244353;
        				}
        			}
        		}
        		cout<<(f[n]+g[n])%998244353<<endl;
        	}
        	return 0;
        }
        
    • \(100pts\) :组合。
      • 依据容斥原理,有第 \(x\) 头是母牛的方案数 \(=\) 总方案数 \(-\)\(x\) 头是公牛的方案数。
        • 总方案数:设公牛有 \(j(0 \le i \le n)\) 头,
          • \(j=0\) 时,显然只有 \(1\) 种方案。
          • \(j \ne 0\) 时,需要固定 \((j-1)(k-1)\) 头母牛,等价于求 \(\sum\limits_{i=1}^{j+1}x_i=n-j-(j-1)(k-1)\) 的非负正整数解的数量,易知其数量为 \(\dbinom{n-j-(j-1)(k-1)+j+1-1}{j+1-1}\)
        • \(x\) 头是公牛的方案数:
          • \(x\) 头是公牛需要 \(x-k+1 \sim x-1,x+1 \sim x+k-1\) 是母牛。
          • \(1 \sim x-k\) 的方案数:设公牛有 \(j(0 \le j \le x-k)\) 头,
            • \(j=0\) 时,显然只有 \(1\) 种方案。
            • \(j \ne 0\) 时,需要固定 \((j-1)(k-1)\) 头母牛,等价于求 \(\sum\limits_{i=1}^{j+1}x_i=x-k-j-(j-1)(k-1)\) 的非负正整数解的数量,易知其数量为 \(\dbinom{x-k-j-(j-1)(k-1)+j+1-1}{j+1-1}\)
          • \(x+k \sim n\) 的方案数:设公牛有 \(j(0 \le j \le n-(x+k-1))\) 头,
            • \(j=0\) 时,显然只有 \(1\) 种方案。
            • \(j \ne 0\) 时,需要固定 \((j-1)(k-1)\) 头母牛,等价于求 \(\sum\limits_{i=1}^{j+1}x_i=n-(x+k-1)-j-(j-1)(k-1)\) 的非负正整数解的数量,易知其数量为 \(\dbinom{n-(x+k-1)-j-(j-1)(k-1)+j+1-1}{j+1-1}\)
      • 求一下边界, \(\sum\limits_{j=0}^{\left\lfloor\dfrac{n+k-1}{k-1}\right\rfloor}\dbinom{n-j-(j-1)(k-1)+j+1-1}{j+1-1}-(\sum\limits_{j=0}^{\left\lfloor\dfrac{x-k+k-1}{k-1}\right\rfloor}\dbinom{x-k-j-(j-1)(k-1)+j+1-1}{j+1-1} \times \sum\limits_{j=0}^{\left\lfloor\dfrac{n-(x+k-1)+k-1}{k-1}\right\rfloor}\dbinom{n-(x+k-1)-j-(j-1)(k-1)+j+1-1}{j+1-1})\) 即为所求。
      • 预处理时间复杂度为 \(O(n)\) ,单次时间复杂度为 \(O(\dfrac{n}{k})\)
        • 学校数据太水了,导致此做法过了。
          • 极限数据为 \(\dfrac{\dfrac{100000 \times (100000+1)}{2}}{2}\)\(hack\) 数据生成器如下。
            int main()
            {
                cout<<100000<<" "<<100000<<endl;
                for(int i=1;i<=100000;++i)
                {
                    cout<<i<<" "<<2<<endl;
                }
            }
            
        ll jc[200001],inv[200001],jc_inv[200001];
        ll C(ll n,ll m,ll p)
        {
            if(n>=m&&n>=0&&m>=0)
            {
                return (jc[n]*jc_inv[m]%p)*jc_inv[n-m]%p;
            }
            else
            {
                return 0;
            }
        }
        ll qpow(ll a,ll b,ll p)
        {
            ll ans=1;
            while(b>0)
            {
                if(b&1)
                {
                    ans=ans*a%p;
                }
                b>>=1;
                a=a*a%p;
            }
            return ans;
        }
        int main()
        {
            ll n,q,i,j,x,k,p=998244353,sum1,sum2,sum3;
            cin>>n>>q;
            inv[1]=1;
            jc[0]=jc_inv[0]=jc[1]=jc_inv[1]=1;
            for(i=2;i<=n;i++)
            {
                inv[i]=(p-p/i)*inv[p%i]%p;
                jc[i]=jc[i-1]*i%p;
                jc_inv[i]=jc_inv[i-1]*inv[i]%p;
            }
            for(i=1;i<=q;i++)
            {
                cin>>x>>k;
                if(k==1)
                {
                    cout<<qpow(2,n-1,p)<<endl;
                }
                else
                {
                    sum1=sum2=sum3=1;
                    for(j=1;j<=(n+k-1)/(k-1);j++)
                    {
                        sum1=(sum1+C(n-j-(j-1)*(k-1)+j+1-1,j+1-1,p))%p;
                    }
                    for(j=1;j<=(x-k+k-1)/(k-1);j++)
                    {
                        sum2=(sum2+C(x-k-j-(j-1)*(k-1)+j+1-1,j+1-1,p))%p;
                    }
                    for(j=1;j<=(n-(x+k-1)+k-1)/(k-1);j++)
                    {
                        sum3=(sum3+C(n-(x+k-1)-j-(j-1)*(k-1)+j+1-1,j+1-1,p))%p;
                    }
                    cout<<(sum1-sum2*sum3%p+p)%p<<endl;
                }
            }
            return 0;
        }
        
  • 正解
    • 根号分治
      • @wkh2008 瑞平 \(T4\)

      • \(k> \sqrt{n}\) 时,考虑到组合做法难以进行进一步预处理,故使用组合做法。

      • \(k \le \sqrt{n}\) 时,考虑到对计数类型的 \(DP\) 做法利用容斥原理进一步预处理。对于第 \(j(1 \le j \le q)\) 次集会,令 \(f_i(0 \le i \le n)\) 表示第 \(i\) 头是母牛的方案数, \(g_i(0 \le i \le n)\) 表示第 \(i\) 头是公牛的方案数。由定义,有 \(f_{0,k}=g_{0,k}=0,f_{1,k}=g_{1,k}=1\) 。容易对于 \(2 \le i \le n\) ,有递推式 \(f_{i,k}=f_{i-1,k}+g_{i-1,k},g_{i,k}=\begin{cases} 1 & i \le k \\ f_{i-k,k}+g_{i-k,k} & i>k \end{cases}\) 。最终, \(f_{n,k}+g_{n,k}-(f_{\max(x-k,0),k}+g_{\max(x-k,0),k}) \times (f_{\max(n-(x+k-1),0),k}+g_{\max(n-(x+k-1),0),k})\) 即为所求。

        • 本质上是对重复出现的 \(k\) 进行预处理。
      • 空间复杂度为 \(O(n \sqrt{n})\) ;预处理时间复杂度为 \(O(n \sqrt{n})\) ,单次查询时间复杂度为 \(O(\sqrt{n})\)

        • 因为 原代码 常数较大,在加入超级快读和卡测评机的时间后仅可取得 \(70pts\) 。故最终将边界由 \(\sqrt{n}\) 改为 \(20\)
        ll jc[100001],inv[100001],jc_inv[100001],f[100001][320],g[100001][20],vis[20];
        ll C(ll n,ll m,ll p)
        {
            if(n>=m&&n>=0&&m>=0)
            {
                return (jc[n]*jc_inv[m]%p)*jc_inv[n-m]%p;
            }
            else
            {
                return 0;
            }
        }
        int main()
        {
            ll n,q,i,j,x,k,p=998244353,sum1,sum2,sum3;
            cin>>n>>q;
            inv[1]=1;
            jc[0]=jc_inv[0]=jc[1]=jc_inv[1]=1;
            for(i=2;i<=n;i++)
            {
                inv[i]=(p-p/i)*inv[p%i]%p;
                jc[i]=jc[i-1]*i%p;
                jc_inv[i]=jc_inv[i-1]*inv[i]%p;
            }
            for(i=1;i<=q;i++)
            {
                cin>>x>>k;
                if(k<=20)
                {
                    if(vis[k]==0)
                    {
                        vis[k]=1;
                        f[1][k]=g[1][k]=1;
                        for(j=2;j<=n;j++)
                        {
                            f[j][k]=(f[j-1][k]+g[j-1][k])%p;
                            if(j<=k)
                            {
                                g[j][k]=1;
                            }
                            else
                            {
                                g[j][k]=(f[j-k][k]+g[j-k][k])%p;
                            }
                        }
                    }
                    sum1=(f[n][k]+g[n][k])%p;
                    sum2=(f[max(x-k,0ll)][k]+g[max(x-k,0ll)][k])%p;
                    sum3=(f[max(n-(x+k-1),0ll)][k]+g[max(n-(x+k-1),0ll)][k])%p;
                }
                else
                {
                    sum1=sum2=sum3=1;
                    for(j=1;j<=(n+k-1)/(k-1);j++)
                    {
                        sum1=(sum1+C(n-j-(j-1)*(k-1)+j+1-1,j+1-1,p))%p;
                    }
                    for(j=1;j<=(x-k+k-1)/(k-1);j++)
                    {
                        sum2=(sum2+C(x-k-j-(j-1)*(k-1)+j+1-1,j+1-1,p))%p;
                    }
                    for(j=1;j<=(n-(x+k-1)+k-1)/(k-1);j++)
                    {
                        sum3=(sum3+C(n-(x+k-1)-j-(j-1)*(k-1)+j+1-1,j+1-1,p))%p;
                    }
                }
                cout<<(sum1-max(sum2,1ll)*max(sum3,1ll)%p+p)%p<<endl;
            }
            return 0;
        }
        

总结

  • \(T1,T3,T4\) 打到 \(8:30,9:30\) 打完 \(T2\) 后就去打高斯消元了,导致没有看见 \(T3\) 的对 \(2^{64}\) 取模(打成了 \(2^{60}\) ),挂了 \(30pts\)
  • \(T3\) 觉得线段树可做,但不想打了。
  • \(T4\) 没有再多想想,没骗到计数类型的 \(DP\) 分。推式子的时候应注意更加模块化,防止因为推式子多推一步而导致无法得到正确式子。
posted @ 2023-11-17 19:08  hzoi_Shadow  阅读(154)  评论(2)    收藏  举报
扩大
缩小