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';
}