Atcoder Regular Contest 选做
终于和自己真正打过的 ARC 的场次接上了 qwq。之后的更新可能会比较随机。
ARC142 E Pairing Wizards
更有趣题,做了 114514 年。最后是瞄了眼 fzw 代码突然就会了
发现这个数据范围非常 flow 啊,考虑一些图论转化。称最后 \(a_u\geq b_u\) 的点为好点。那么你发现坏点的出现条件其实是比较苛刻的。一个点成为坏点那么它至少要 \(a_u\) 比邻域所有的 \(b\) 还要大,为了避免成为好点它的 \(b\) 应该也要比这个最大值要大。
这么说,你找出那种 \(b\) 局部最大的点,最终坏点一定是这些点的子集。这告诉我们坏点实际上互不相邻,而好点之间的边肯定是不用管的。这张图现在成了二分图,更加 flow 了。考虑先算一下好点坏点哪些不得不达到的限制,先把这一部分的贡献加到答案上。剩下的限制形如要么 \(a_u\geq b_u\) 要么 \(a_v\geq b_u\)。
啊这个东西好难做啊?发现值域很小?那么转成 01 序列每个都 flow 一下求出点覆盖加起来?这个彩币交了上去发现这个做法是假的。
看了眼场上代码你发现这个东西本质上是要对右部点计算一个 \(\max\) 的贡献,点覆盖对值域拆点之后可以很好支持这种结构。具体地,每个右部点拆成一列点,每条边向一个这列点的前缀连边,这样最小割的时候一定是割这列点所有边权取 \(\max\) 的前缀。
感觉这个套路很牛,这告诉我们最小割如何处理有关 \(\max\) 的贡献。
Code
#include <cstdio>
#include <algorithm>
#include <atcoder/maxflow.hpp>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=103;
int n,m;
int a[N],b[N];
vector<int> vec[N];
bool sp[N];
int id[N],cnt;
int main(){
n=read();
for(int i=1;i<=n;++i){a[i]=read();b[i]=read();}
m=read();
for(int i=1;i<=m;++i){
int u=read(),v=read();
vec[u].emplace_back(v);
vec[v].emplace_back(u);
}
int res=0;
int s=cnt++,t=cnt++;
for(int u=1;u<=n;++u){
sp[u]=1;
for(int v:vec[u])
if(b[v]>=b[u]){sp[u]=0;break;}
if(sp[u]){
for(int v:vec[u]) if(a[u]<b[v]) res+=b[v]-a[u],a[u]=b[v];
id[u]=cnt++;
}
else{
if(a[u]<b[u]) res+=b[u]-a[u],a[u]=b[u];
id[u]=cnt;
cnt+=100-a[u];
}
}
atcoder::mf_graph<int> mf(cnt);
for(int u=1;u<=n;++u){
if(sp[u]){
if(a[u]<b[u]){
mf.add_edge(s,id[u],b[u]-a[u]);
for(int v:vec[u])
if(a[v]<b[u]){
for(int i=0;i<b[u]-a[v];++i) mf.add_edge(id[u],id[v]+i,0x3f3f3f3f);
}
}
}
else{
for(int i=0;i<100-a[u];++i) mf.add_edge(id[u]+i,t,1);
}
}
printf("%d\n",res+mf.flow(s,t));
return 0;
}
ARC142 D Deterministic Placing
有趣题,做了蛮久的。
考虑这样一件事,你既然只有唯一的状态,而操作可逆,每次返回上一个状态一定是合法的,容易得到合法的状态就是在两个状态间反复横跳。
那么一个黑点的轨道只有两个点,再次刻画操作形态,发现如果我们只考虑轨道相交的黑点构成的连通块,那么这个联通块一定是一条链,而且这条链恰好有两种初始状态。
于是我们就转化为把树划分成若干条长度 \(>1\) 的链。但这并不充分,我们还限制了一条链的端点不能接着另一条链的非端点。这些限制可以用简单的树形 DP 刻画。稍有细节。
Code
#include <cstdio>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=200003,P=998244353;
typedef long long ll;
int hd[N],ver[N<<1],nxt[N<<1],tot;
inline void add(int u,int v){nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v;}
int n;
int f[N],g[N],h[N],s[N];
// f - > head
// g - > body
// h - > tail
// s - > cap
void dfs(int u,int fa){
h[u]=1;
int prod=1,tmp=0;
for(int i=hd[u];i;i=nxt[i]){
int v=ver[i];
if(v==fa) continue;
dfs(v,u);
f[u]=((ll)h[u]*(g[v]+h[v])+(ll)f[u]*f[v])%P;
g[u]=((ll)prod*(g[v]+h[v])+(ll)g[u]*s[v])%P;
s[u]=((ll)tmp*(g[v]+h[v])+(ll)s[u]*s[v])%P;
tmp=((ll)prod*(g[v]+h[v])+(ll)tmp*s[v])%P;
prod=(ll)prod*s[v]%P;
h[u]=(ll)h[u]*f[v]%P;
}
s[u]<<=1;if(s[u]>=P) s[u]-=P;
}
int main(){
n=read();
for(int i=1;i<n;++i){
int u=read(),v=read();
add(u,v);add(v,u);
}
dfs(1,0);
int res=f[1];
res<<=1;
if(res>=P) res-=P;
res+=s[1];
if(res>=P) res-=P;
printf("%d\n",res);
return 0;
}
ARC141 F Well-defined Abbreviation
神题!不会一点。
考虑什么样的串集是 Yes
。一个直观的感受是如果这些串之间无交,显然删除它们的顺序无关紧要。否则如果存在一个串的后缀等于另一个串的前缀,看起来就出现了多种不同的删法。
然而直接这样是不准确的,比如 AB、ABAB 两个串有交但显然删除方式唯一。我们发现这样总是因为一些串的贡献被另一些串等效替代了。这提示我们先去删除哪些非本原串,就是可以通过其它串删除出来的串。
如何判断非本原串呢?我们可以先建 AC 自动机,然后拿当前的串在 AC 自动机上走,如果匹配到一个后缀那么就删除这个后缀,即回退到还没加入这个后缀的点。如果最终删空了就是非本原串了!
这时候我们想:如果删了但是没删空呢?这个时候你一开始就可以直接把这个串删掉,也就是说从这个串删可以删出空集和非空集,那么这个串本身就是题目中要找的串!!
这意味着,题目中需要我们判定的本原串互不包含。那么此时我们一开始的结论就非常对了!吗?你发现 ABC 和 BCA 依旧只有一种删法,因为任意一个 ABCA 都只会删剩下一个 A。所以你需要开个 map
特判一下这种情况。
Code
#include <cstdio>
#include <cstring>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/hash_policy.hpp>
using namespace std;
const int N=1000003,M=2000003;
__gnu_pbds::gp_hash_table<int,int> mp[M];
int n,len[N];
char *s[N],ss[M];
int stk[M],tp;
int tr[M][4],cnt=1;
int fail[M],de[M],que[M],tl;
bool ed[M],exi[M];
int jump[M],pos[M],id[M];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%s",ss);
len[i]=strlen(ss);
s[i]=new char[len[i]];
memcpy(s[i],ss,len[i]);
int p=1;
for(int j=0;j<len[i];++j){
int c=ss[j]-'A';
if(!tr[p][c]) de[tr[p][c]=++cnt]=j+1;
p=tr[p][c];
}
ed[pos[i]=p]=1;
}
for(int c=0;c<4;++c) tr[0][c]=1;
que[tl=1]=1;
for(int i=1;i<=tl;++i){
int u=que[i];
if(ed[u]) jump[u]=de[u];
if(jump[fail[u]]) jump[u]=jump[fail[u]];
for(int c=0;c<4;++c){
if(tr[u][c]) fail[que[++tl]=tr[u][c]]=tr[fail[u]][c];
else tr[u][c]=tr[fail[u]][c];
}
}
for(int i=1;i<=n;++i){
stk[tp=0]=1;exi[i]=1;
for(int j=0;j<len[i];++j){
int c=s[i][j]-'A';
stk[tp+1]=tr[stk[tp]][c];++tp;
if(jump[stk[tp]]){
if(tp<len[i]) exi[i]=0;
tp-=jump[stk[tp]];
}
}
if(tp){puts("Yes");return 0;}
}
for(int i=1;i<=n;++i)
if(exi[i]){
for(int j=0,p=1;j<len[i];++j){
int c=s[i][j]-'A';
p=tr[p][c];
if(id[p]) id[p]=-1;
else id[p]=i;
}
}
for(int i=1;i<=n;++i)
if(exi[i]){
int p=pos[i];
while(fail[p]>1){
p=fail[p];
if(id[p]<0){puts("Yes");return 0;}
int j=id[p];
if(!j) continue;
mp[i][de[p]]=j;
if(len[i]!=len[j]){puts("Yes");return 0;}
}
}
for(int i=1;i<=n;++i)
if(exi[i]){
int p=pos[i];
while(fail[p]>1){
p=fail[p];
int j=id[p];
if(!j) continue;
if(mp[j][len[i]-de[p]]!=i){puts("Yes");return 0;}
}
}
puts("No");
return 0;
}
ARC141 E Sliding Edge on Torus
这种题已经被出成套路了。
对每一条斜线等价类建出点,你考虑这个操作本质上相当于是说,你一开始在一个点 \(x\),状态为 \(y\)。每走过一条边状态可以变成 \((y+w)\bmod n\),问你最后能到达那些点和状态的二元组。
这个问题求出原图所有环长的 \(\gcd\) 有关。建出生成树只对只有一条非树边的环做就行了。
动态维护直接带权并查集。
Code
#include <cstdio>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=200003;
typedef long long ll;
int n,q;
int f[N],g[N],w[N];
int gcd(int a,int b){
if(!b) return a;
return gcd(b,a%b);
}
inline void link(int u){
if(f[u]==u) return;
link(f[u]);
w[u]=(w[u]+w[f[u]])%n;
f[u]=f[f[u]];
}
int main(){
n=read();q=read();ll res=0;
for(int i=0;i<n;++i) f[i]=i,g[i]=n,w[i]=0,res+=n;
for(int i=0;i<q;++i){
int a=read(),b=read(),c=read(),d=read();
int x=(b-a+n)%n,y=(d-c+n)%n,z=(d-b+n)%n;
link(x);link(y);
if(f[x]!=x){z=(z+n-w[x])%n;x=f[x];}
if(f[y]!=y){z=(z+w[y])%n;y=f[y];}
if(x==y){
res-=g[x];
g[x]=gcd(g[x],z);
res+=g[x];
}
else{
res-=g[x];res-=g[y];
f[x]=y;w[x]=z;g[y]=gcd(g[x],g[y]);
res+=g[y];
}
printf("%lld\n",res);
}
return 0;
}
ARC141 D Non-divisible Set
这个 D 好有趣。这场感觉 D>E 了。
考虑题目本质是让你在偏序图中选一个最长反链,那么我们就来刻画最长反链的性质。
猜测即使对于 \(2M\) 个点的图,最长反链也是 \(M\)。证明考虑最长反链不超过任何一种链覆盖,而我们可以直接构造出大小为 \(M\) 的链覆盖:从每个奇数开始往它的两倍连边。
想要做这道题,得知道最长反链为什么不超过任何一种链覆盖。这是因为每一条链你反链最多只能选一个点。那我们瞧瞧上面那个构造,这就相当于我们构造出来的反链集合除去所有的偶因子之后正好可以得到全体奇数。
现在考虑先把奇数的偏序图直接建出来,再决策每个奇数乘了多少个二。不难发现想要保证它是反链,需要满足对于两个奇数 \(x|y\),\(v_2(x)>v_2(y)\)。这是就转化成关于 \(v_2(x)\) 的一张偏序关系图。
原题相当于是问每一个 \(x\),\(v_2(x)\) 能取哪些值。那么顺着拓扑再逆着拓扑就可以求出 \(v_2(x)\) 的上下界了。
Code
#include <queue>
#include <cstdio>
#include <vector>
#include <algorithm>
#pragma GCC optimize(2,3,"Ofast")
using namespace std;
queue<int> que;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=1000003,INF=0x3f3f3f3f;
int hd[N],ver[N<<1],nxt[N<<1],tot;
inline void add(int u,int v){nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v;}
int n,m;
int a[N],b[N];
vector<int> vec[N];
vector<int> F[N],G[N];
int df[N],dg[N];
int f[N],g[N];
int main(){
n=read();m=read();
for(int i=0;i<m;++i) vec[i].emplace_back(-INF);
for(int i=1;i<=n;++i){
a[i]=read();
while(~a[i]&1) a[i]>>=1,++b[i];
vec[a[i]>>=1].emplace_back(b[i]);
}
for(int i=0;i<m;++i){
if(vec[i].size()==1lu){
while(n--) puts("No");
return 0;
}
vec[i].emplace_back(INF);
}
for(int i=0;i<m;++i){
int x=i<<1|1;
for(int y=x*3;y<m*2;y+=x*2){
int j=y>>1;
F[j].emplace_back(i);++df[i];
G[i].emplace_back(j);++dg[j];
}
}
for(int i=0;i<m;++i){f[i]=-INF;if(!df[i]) que.emplace(i);}
while(!que.empty()){
int u=que.front();que.pop();
if(f[u]!=INF) f[u]=*upper_bound(vec[u].begin(),vec[u].end(),f[u]);
if(f[u]==INF){while(n--) puts("No");return 0;}
for(int v:F[u]){
if(!--df[v]) que.emplace(v);
if(f[v]<f[u]) f[v]=f[u];
}
}
for(int i=0;i<m;++i){g[i]=INF;if(!dg[i]) que.emplace(i);}
while(!que.empty()){
int u=que.front();que.pop();
if(g[u]!=-INF) g[u]=*prev(lower_bound(vec[u].begin(),vec[u].end(),g[u]));
if(g[u]==INF){while(n--) puts("No");return 0;}
for(int v:G[u]){
if(!--dg[v]) que.emplace(v);
if(g[v]>g[u]) g[v]=g[u];
}
}
for(int i=1;i<=n;++i){
if(f[a[i]]<=b[i]&&b[i]<=g[a[i]]) puts("Yes");
else puts("No");
}
return 0;
}
ARC140 F ABS Permutation
不想写 GF 题了,口胡吧。转置条件为 \(|p_i-p_{i+m}|=1\),按下标分组之后就变成了 \(m=1\) 的问题,把 EGF 快速幂起来就行了。
\(m=1\) 的问题考虑这样做:直接拿着单位排列,然后劈成若干段,每一段可以选择 reverse
,最后重排,上个容斥就可以统计方案了。
ARC140 E Not Equal Rectangle
感觉是一道非常好的构造题啊!不存在四个角颜色相同的子矩形等价于假设我们只考虑一种颜色的放置,对于所有的无序二元组 \((x,y)\) 至多只能在一行同时满足 \(x,y\) 同时是该颜色。
相当于给你个团,你要去划分出若干个团满足边集不交。注意到每种颜色都是没区别的,我们可以猜测在最终的构造中每种颜色是尽量平均的,注意到颜色数差不多是根号,我们考虑一个让每种颜色每一行都出现根号次恰好能填满。
现在的问题是你需要把一个 \(B^2\) 级别的团尽可能分解成大小 \(B\) 的团。这玩意在 \(B=25\) 时不太好做?但在 \(B=23\) 时是可以数论构造的!\(23^2>500\) 所以只用 \(23\) 种颜色理论可行。
数论的构造方法大概就是你将所有元素分成个数相同的 \(B\) 组,记第 \(i\) 组第 \(j\) 个为 \(p_{i,j}\)(0-indexed)。然后你可以对每一个 \(x,y\in [0,n)\),构造出 \(\{p_{i,(x+yi)\bmod B}|i\in [0,n)\cap \Z\}\) 这个团。
Code
#include <cstdio>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int B=23,N=529;
int n,m;
int f[N][N];
int main(){
for(int i=0;i<B;++i)
for(int j=0;j<B;++j)
for(int k=0;k<B;++k){
int x=(i+k)%B;
for(int t=0;t<B;++t){
f[i*B+j][t*B+x]=k;
x=(x+j)%B;
}
}
n=read();m=read();
for(int i=0;i<n;++i){
for(int j=0;j<m;++j) printf("%d ",f[i][j]+1);
putchar('\n');
}
return 0;
}
ARC140 D One to One
发现如果用常规计数方法容斥去算连通块个数非常难以优化。但注意到这道题的图是基环森林,所以连通块个数就是环的个数,而环你是可以独立开来算的,其它的边不会影响它是否是一个环。
剩下的就简单了,DP 一下链拼成环的方案数就做完了。
Code
#include <cstdio>
using namespace std;
int read(){
char c=getchar();int x=0;bool f=0;
while(c<48||c>57) f|=(c=='-'),c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
if(f) return -x;
return x;
}
const int N=2003,P=998244353;
typedef long long ll;
int f[N];
int rt(int x){
if(f[x]==x) return x;
return f[x]=rt(f[x]);
}
int n,cnt;
int dp[N],sz[N],pw[N];
bool cir[N];
inline void merge(int x,int y){
x=rt(x);y=rt(y);
if(x==y){
cir[x]=1;
return;
}
cir[x]|=cir[y];f[y]=x;
}
int main(){
n=read();
dp[0]=1;
for(int i=1;i<=n;++i) f[i]=i;
for(int i=1;i<=n;++i){
int x=read();
if(~x) merge(x,i);
else ++cnt;
}
for(int i=1;i<=n;++i) ++sz[rt(i)];
int lim=0,res=0;
dp[0]=1;
for(int i=1;i<=n;++i)
if(f[i]==i){
if(cir[i]) ++res;
else{
for(int x=lim++;~x;--x)
dp[x+1]=(dp[x+1]+(ll)dp[x]*sz[i])%P;
}
}
pw[0]=1;
for(int i=1;i<=cnt;++i) pw[i]=(ll)pw[i-1]*n%P;
res=(ll)res*pw[cnt]%P;
for(int i=1,fac=1;i<=lim;++i){
res=(res+(ll)pw[cnt-i]*fac%P*dp[i])%P;
fac=(ll)fac*i%P;
}
printf("%d\n",res);
return 0;
}
ARC138 E Decreasing Subsequence
不要问为啥 138 在 139 后面写。
我认为是相当神的,至少完全想不到这个抽象双射。
对于非零位置 \(i\),连边 \(i\to a_i-1\)。只有连向零的边会成为链头。于是张图一定是若干条链。
链划分就是无序集划分,递减子序列相当于找一个长度为 \(2k\) 的序列 \(S\) 满足 对于 \(i>k\) 有 \(S_i\) 向 \(S_{2k-i+1}\) 连边。这也意味着这是回文配对的两个节点是链上两个相邻的节点。
这意味这我们可以把经过这个下降子序列的链在这个位置断开。原图的链就分成了三个集合:没有被断开的那些链若干条,断开后左边的那些链正好 \(k\) 条,断开后右边的那些链正好 \(k\) 条。
枚举这三个链的点集并大小,然后用斯特林数和组合数加上贝尔数随便算算就行了。
Code
#include <cstdio>
using namespace std;
typedef long long ll;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=5003,P=1000000007;
int n,k;
int s[N][N],c[N][N],b[N];
int main(){
n=read()+1;k=read();
c[0][0]=s[0][0]=b[0]=1;
for(int i=1;i<=n;++i){
c[i][0]=1;
for(int j=1;j<=i;++j){
s[i][j]=(s[i-1][j-1]+(ll)s[i-1][j]*j)%P;
c[i][j]=c[i-1][j-1]+c[i-1][j];
if(c[i][j]>=P) c[i][j]-=P;
b[i]+=s[i][j];
if(b[i]>=P) b[i]-=P;
}
}
int res=0;
for(int i=k;i<=n;++i)
for(int j=k;i+j<=n;++j)
res=(res+(ll)c[n][i+j]*s[i][k]%P*s[j][k]%P*b[n-i-j])%P;
printf("%d\n",res);
return 0;
}
ARC139 E Wazir
有些细节有点抽象,被卡了好久233。
这个题首先 \(n,m\) 均为偶数答案是 \(2\)。否则你考虑奇数长度的那一维最多选满也就 \(\frac{len-1}{2}\),所以你可以说当 \(n,m\) 都是奇数时 \(\min(n \frac{m-1}{2},m \frac{n-1}{2})\) 是个上界。
抽象的是,这竟然就是可以达到的。你考虑如何去刻画奇数环的最大染色方案 .#.#..#.#.
,发现可以找到唯一的哪个 ..
,只用记录它的位置就行了。所以说单个奇环的方案数是长度。
每个方案 \(x\) 能够相邻的方案发现只有 \(x-1\) 与 \(x+1\)。这里的 \(\pm 1\) 都是环上的,也就是模意义下的。
所以说有一种方案等价于能找出一个数组,长度为 \(m\),在模 \(n\) 意义下相邻两个位置(开头结尾认为相邻)只相差 \(1\)。如果 \(m\) 为偶数显然可以找一半的 \(+1\) 与一半的 \(-1\) 构造出来。\(m\) 为奇数 \(m<n\) 根据奇偶性显然是没啥办法的。但这个时候 \(n,m\) 都是奇数,所以你转置过来考虑那个更紧的限制依然找得到方案。
统计方案就是循环卷积快速幂的板子了。随便做做。
Code
#include <cstdio>
#include <algorithm>
using namespace std;
const int P=998244353;
typedef long long ll;
ll n,m;
typedef long long ll;
int qp(int a,int b=P-2){
int res=1;
while(b){
if(b&1) res=(ll)res*a%P;
a=(ll)a*a%P;b>>=1;
}
return res;
}
int rev[1<<18],cw[1<<18|1];
int len,ilen;
void init(int _len){
len=1;int bt=-1;
while(len<_len) len<<=1,++bt;
cw[len]=cw[0]=1;
int w=qp(3,(P-1)>>(bt+1));
ilen=qp(len);
for(int i=1;i<len;++i){
rev[i]=(rev[i>>1]>>1)|((i&1)<<bt);
cw[i]=(ll)cw[i-1]*w%P;
}
}
void NTT(int *arr){
for(int i=0;i<len;++i)
if(i<rev[i]) swap(arr[i],arr[rev[i]]);
for(int i=1,t=1;i<len;i<<=1,++t)
for(int j=0;j<len;j+=(i<<1))
for(int k=j,tt=0;k<(j|i);++k,tt+=(len>>t)){
int x=arr[k],y=(ll)arr[k|i]*cw[tt]%P;
if((arr[k]=x+y)>=P) arr[k]-=P;
if((arr[k|i]=x-y)<0) arr[k|i]+=P;
}
}
void INTT(int *arr){
for(int i=0;i<len;++i)
if(i<rev[i]) swap(arr[i],arr[rev[i]]);
for(int i=1,t=1;i<len;i<<=1,++t)
for(int j=0;j<len;j+=(i<<1))
for(int k=j,tt=len;k<(j|i);++k,tt-=(len>>t)){
int x=arr[k],y=(ll)arr[k|i]*cw[tt]%P;
if((arr[k]=x+y)>=P) arr[k]-=P;
if((arr[k|i]=x-y)<0) arr[k|i]+=P;
}
for(int i=0;i<len;++i) arr[i]=(ll)arr[i]*ilen%P;
}
const int N=1000003;
int S[N],R[N];
int fiv[N],fac[N];
int main(){
scanf("%lld%lld",&n,&m);
if(~n&~m&1){
puts("2");
return 0;
}
if((n&m&1)&&n>m) swap(n,m);
if(~n&m&1) swap(n,m);
if(n>m){
fac[0]=1;
for(int i=1;i<=m;++i) fac[i]=(ll)fac[i-1]*i%P;
fiv[m]=qp(fac[m]);
for(int i=m;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
int res=0;
for(int i=0;i<=m;++i)
if((i+i-m)%n==0) res+=(ll)fiv[i]*fiv[m-i]%P;
res=(ll)res*fac[m]%P*(n%P)%P;
printf("%d\n",res);
return 0;
}
int res=n;
S[1]=S[n-1]=1;
R[0]=1;
init(n<<1);
while(m){
NTT(S);
if(m&1){
NTT(R);
for(int i=0;i<len;++i) R[i]=(ll)R[i]*S[i]%P;
INTT(R);
for(int i=n+n-1;i>=n;--i) {
R[i-n]+=R[i];R[i]=0;
if(R[i-n]>=P) R[i-n]-=P;
}
}
for(int i=0;i<len;++i) S[i]=(ll)S[i]*S[i]%P;
INTT(S);
for(int i=n+n-1;i>=n;--i){
S[i-n]+=S[i];S[i]=0;
if(S[i-n]>=P) S[i-n]-=P;
}
m>>=1;
}
res=(ll)res*R[0]%P;
printf("%d\n",res);
return 0;
}
ARC139 D Priority Queue 2
感觉这场 B>D?加数删数一律考虑转 01,转成 01 之后就变成了折线问题,相当于是每一次有一定的概率靠近一条线,求最后位置的期望。
这个东西好像很不好优化?什么数据范围只有 2000?那不直接暴力 \(O(K)\) 算就行了?
Code
#include <cstdio>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=2003,P=998244353;
typedef long long ll;
int n,m,k,s,res;
int a[N],pw[N];
int c[N][N];
int main(){
n=read();m=read();k=read();s=n+1-read();
for(int i=1;i<=n;++i) a[i]=read();
for(int i=0;i<=k;++i){
c[i][0]=1;
for(int j=1;j<=i;++j){
c[i][j]=c[i-1][j-1]+c[i-1][j];
if(c[i][j]>=P) c[i][j]-=P;
}
}
for(int x=0;x<m;++x){
int st=0;
for(int i=1;i<=n;++i) if(a[i]>x) ++st;
pw[0]=1;
for(int i=1;i<=k;++i) pw[i]=(ll)pw[i-1]*x%P;
for(int i=0,p=1;i<=k;++i,p=(ll)p*(m-x)%P){
int t=st;
if(t>s){t-=k-i;if(t<s) t=s;}
else{t+=i;if(t>s) t=s;}
res=(res+(ll)c[k][i]*pw[k-i]%P*p%P*t)%P;
}
}
printf("%d\n",res);
return 0;
}
ARC137 F Overlaps
也不难,感觉跟之前杭电的一道题很像,只不过这道题直接容斥就可以做了。
\([0,1]\) 数轴上随机就是相当于随一个排列,相当于每次随两个括号插进序列。方案数为 \(n!(2n-1)!!\)
发现左端点的顺序不是很关心,所以除掉一个 \(n!\) 只剩一个双阶乘了。
那么就完全变成括号串带权计数问题了。每次往下走时需要决定与哪个左括号匹配,需要乘一个系数不好处理。
我们考虑找出每一次下降前的那 \(n\) 个点,只有这些点有系数,发现这个序列需要满足每一次下降不超过 \(1\) 且末尾是 \(1\)。reverse 一下序列,容斥掉前面的条件,容斥后就变成有若干条递增的链不能选值域相邻的两个数。这是分治 NTT 经典题,直接做就完了。
如何每个链拼容斥的系数呢?求个逆就好了。
Code
#include <array>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int P=998244353,N=1<<20;
typedef long long ll;
int qp(int a,int b=P-2){
int res=1;
while(b){
if(b&1) res=(ll)res*a%P;
a=(ll)a*a%P;b>>=1;
}
return res;
}
int rev[N],cw[N+1];
int len,ilen;
void init(int _len){
len=1;int bt=-1;
while(len<_len) len<<=1,++bt;
cw[len]=cw[0]=1;
int w=qp(3,(P-1)>>(bt+1));
ilen=qp(len);
for(int i=1;i<len;++i){
rev[i]=(rev[i>>1]>>1)|((i&1)<<bt);
cw[i]=(ll)cw[i-1]*w%P;
}
}
void NTT(int *arr){
for(int i=0;i<len;++i)
if(i<rev[i]) swap(arr[i],arr[rev[i]]);
for(int i=1,t=1;i<len;i<<=1,++t)
for(int j=0;j<len;j+=(i<<1))
for(int k=j,tt=0;k<(j|i);++k,tt+=(len>>t)){
int x=arr[k],y=(ll)arr[k|i]*cw[tt]%P;
if((arr[k]=x+y)>=P) arr[k]-=P;
if((arr[k|i]=x-y)<0) arr[k|i]+=P;
}
}
void INTT(int *arr){
for(int i=0;i<len;++i)
if(i<rev[i]) swap(arr[i],arr[rev[i]]);
for(int i=1,t=1;i<len;i<<=1,++t)
for(int j=0;j<len;j+=(i<<1))
for(int k=j,tt=len;k<(j|i);++k,tt-=(len>>t)){
int x=arr[k],y=(ll)arr[k|i]*cw[tt]%P;
if((arr[k]=x+y)>=P) arr[k]-=P;
if((arr[k|i]=x-y)<0) arr[k|i]+=P;
}
for(int i=0;i<len;++i) arr[i]=(ll)arr[i]*ilen%P;
}
int tmp[N];
void INV(int *arr,int *res,int _len){
res[0]=qp(arr[0]);
for(int t=1;t<_len;t<<=1){
init(t<<2);
for(int i=0;i<(t<<1);++i) tmp[i]=arr[i];
for(int i=(t<<1);i<len;++i) tmp[i]=0;
NTT(res);NTT(tmp);
for(int i=0;i<len;++i){
res[i]=(2-(ll)tmp[i]*res[i])%P*res[i]%P;
if(res[i]<0) res[i]+=P;
}
INTT(res);
for(int i=(t<<1);i<len;++i) res[i]=0;
}
for(int i=_len;i<len;++i) res[i]=0;
}
int X[N],Y[N],Z[N];
struct poly{
vector<int> p;
poly(int len):p(len){}
poly(initializer_list<int> lis):p(lis){}
friend poly operator*(const poly x,const poly y){
int xl=x.p.size(),yl=y.p.size();
init(xl+yl-1);
for(int i=0;i<xl;++i) X[i]=x.p[i];
for(int i=xl;i<len;++i) X[i]=0;
for(int i=0;i<yl;++i) Y[i]=y.p[i];
for(int i=yl;i<len;++i) Y[i]=0;
NTT(X);NTT(Y);
for(int i=0;i<len;++i) Z[i]=(ll)X[i]*Y[i]%P;
INTT(Z);
int zl=len;
while(zl&&!Z[zl-1]) --zl;
poly res(zl);
res.p.resize(zl);
for(int i=0;i<zl;++i) res.p[i]=Z[i];
return res;
}
friend poly operator+(const poly x,const poly y){
int xl=x.p.size(),yl=y.p.size();
int mxl=max(xl,yl);
poly res(mxl);
for(int i=0;i<mxl;++i){
if(i<xl) res.p[i]+=x.p[i];
if(i<yl) res.p[i]+=y.p[i];
if(res.p[i]>=P) res.p[i]-=P;
}
return res;
}
friend poly operator-(const poly x,const poly y){
int xl=x.p.size(),yl=y.p.size();
int mxl=max(xl,yl);
poly res(mxl);
for(int i=0;i<mxl;++i){
if(i<xl) res.p[i]+=x.p[i];
if(i<yl) res.p[i]-=y.p[i];
if(res.p[i]<0) res.p[i]+=P;
}
while(!res.p.empty()&&!res.p.back()) res.p.pop_back();
return res;
}
};
typedef array<poly,4> mat;
mat solve(int l,int r){
if(l==r) return mat({poly({1}),poly(0),poly(0),poly({0,l})});
int mid=(l+r)>>1;
mat lef=solve(l,mid),rig=solve(mid+1,r);
poly l0=lef[0]+lef[1],l1=lef[2]+lef[3];
poly r0=rig[0]+rig[2],r1=rig[1]+rig[3];
return mat({l0*r0-lef[1]*rig[2],l0*r1-lef[1]*rig[3],l1*r0-lef[3]*rig[2],l1*r1-lef[3]*rig[3]});
}
int n,k;
int F[N],G[N];
void inc(int &x,int v){if((x+=v)>=P) x-=P;}
int main(){
n=read();k=read();
mat res=solve(1,k);
for(int i=0;i<=k;++i){
if(i<int(res[0].p.size())) inc(F[i],res[0].p[i]);
if(i<int(res[1].p.size())) inc(F[i],res[1].p[i]);
if(i<int(res[2].p.size())) inc(F[i],res[2].p[i]);
if(i<int(res[3].p.size())) inc(F[i],res[3].p[i]);
}
INV(F,G,n+1);
int ans=0;
for(int i=0;i<=k;++i){
int tmp=0;
if(i<int(res[2].p.size())) inc(tmp,res[2].p[i]);
if(i<int(res[3].p.size())) inc(tmp,res[3].p[i]);
ans=(ans+(ll)G[n-i]*tmp)%P;
}
if(~n&1) ans=P-ans;
int inv=1;
for(int i=1;i<=n;++i) inv=(ll)inv*(i+i-1)%P;
ans=(ll)ans*qp(inv)%P;
printf("%d\n",ans);
return 0;
}
ARC137 E Bakery
放松题,我们考虑最小费用循环流建模,建一条 \(n+1\) 个点直链,第 \(i\) 个点向第 \(i+1\) 个点连容量为 \(A_i\) 代价为 \(-D\) 的边,然后连容量无限代价 0 的边,每次将 \([l,r+1]\) 的区间连成一个环,然后把那个 \(c\) 的贡献放在环上面。直接跑最小费用循环流就可以了。
最小费用循环流类似上下界,先强制所有的负权边满流,然后建虚源汇调整流量就行了。
Code
#include <cstdio>
#include <atcoder/mincostflow.hpp>
using namespace std;
using namespace atcoder;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=2003,INF=0x3f3f3f3f;
typedef long long ll;
int n,m,d;
int a[N];
int main(){
n=read();m=read();d=read();
ll res=0;
mcf_graph<int,ll> mcf(n+3);
for(int i=1;i<=n;++i){
a[i]=read();res+=(ll)d*a[i];
mcf.add_edge(i+1,i,a[i],d);
mcf.add_edge(i,i+1,INF,0);
}
for(int i=1;i<=n+1;++i){
int t=a[i-1]-a[i];
if(t>0) mcf.add_edge(0,i,t,0);
if(t<0) mcf.add_edge(i,n+2,-t,0);
}
for(int i=1;i<=m;++i){
int l=read(),r=read(),c=read();
mcf.add_edge(r+1,l,1,c);
}
res-=mcf.flow(0,n+2).second;
printf("%lld\n",res);
return 0;
}
ARC136 F Flip Cells
明明每个套路都见过合起来就不会做的系列。
但是这道题本身还是非常有意思的!
首先是一个 He_Ren 讲过的套路:对于一个游走过程,设第一次到达某一个好状态的 GF 为 \(F\),从一个好状态到另一个好状态的 GF 为 \(G\),从初始状态到任意一个好状态的 GF 是 \(H\),那么考虑到 \(FG=H\),就有 \(F=\frac{H}{G}\)。也可以直接列出容斥的式子,发现就是求逆的形式。
然后是 PGF 的套路,这道题中 \(F,G,H\) 本质都是 PGF,我们要求的答案是 \(F'(1)\)。由于中间套了个求逆所以妄图先求出 \(F\) 的封闭形式再去求导是不行的。考虑我们利用商的求导法则直接计算 \(G(1),H(1),G'(1),H'(1)\)。
接下来是类似于 Random Transition(当然那道题我也是口胡)的套路。对于这种翻硬币的问题列出每一个硬币的 EGF:\(\frac{e^x\pm e^{-x}}{2}\) 然后考虑乘在一起。又由于要转化成概率所以要复合上一个 \(\frac{1}{nm}x\)
然而 PGF 本质上是一种 OGF,没有 \(\frac{1}{n!}\) 的系数,而生成函数的世界里很难有一种操作能让一个 GF 点乘上一个阶乘。
但发现这道题中的 GF 只有可能是 \(e^{ax}\) 的线性组合这种形式,所以点乘阶乘相当于变成 \(\frac{1}{1-ax}\)。
然而将 \(x=1\) 带进去时又出问题了,\(a=1\) 时 \(\frac{1}{1-ax}\) 没意义。
所以还要对 \(G\) 和 \(H\)同时乘上一个 \((1-x)\),这样对 \(\frac{1-x}{1-ax}\) 求导代入 \(x=1\) 得到 \(\frac{1}{a-1}\) 于是做完了。
Code
#include <cstdio>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int P=998244353,I=499122177;
const int N=53,M=2503;
typedef long long ll;
int qp(int a,int b=P-2){
int res=1;
while(b){
if(b&1) res=(ll)res*a%P;
a=(ll)a*a%P;b>>=1;
}
return res;
}
int n,m,nm;
int c[N][N];
int s[N],a[N];
int res0,res1;
int f[M],len,g[M];
int base[M<<1],*dp=base+M,wd;
int tbase[M<<1],*tdp=tbase+M;
void add(int x,int y){
for(int i=-wd;i<=wd;++i) tdp[i]=dp[i],dp[i]=0;
for(int i=-wd;i<=wd;++i){
dp[i+1]=(dp[i+1]+(ll)tdp[i]*x)%P;
dp[i-1]=(dp[i-1]+(ll)tdp[i]*y)%P;
}
++wd;
}
void del(int x,int y){
for(int i=-wd;i<=wd;++i) tdp[i]=dp[i],dp[i]=0;
--wd;
for(int i=-wd;i<=wd;++i){
dp[i]=(tdp[i-1]-(ll)dp[i-2]*y)%P*x%P;
if(dp[i]<0) dp[i]+=P;
}
}
int inv[M<<1];
void solve(){
f[len=0]=1;
for(int i=1;i<=n;++i){
for(int p=0;p<=len;++p) g[p]=f[p],f[p]=0;
int mx=0;
for(int j=0;j<=s[i];++j){
int t=a[i]-s[i]+j;
if(t>=0&&t<=m-s[i]){
for(int p=0;p<=len;++p)
f[p+j+t]=(f[p+j+t]+(ll)c[s[i]][j]*c[m-s[i]][t]%P*g[p])%P;
if(j+t>mx) mx=j+t;
}
}
len+=mx;
}
res0=0;res1=0;
dp[wd=0]=1;
for(int p=1;p<=nm;++p) add(I,I);
for(int i=0;i<=len;++i){
res0=(res0+(ll)f[i]*dp[nm])%P;
for(int p=-wd;p<=wd;++p)
res1=(res1+(ll)f[i]*dp[p]%P*nm%P*(P-inv[nm-p]))%P;
del(2,I);add(I,I-1);
}
for(int i=-wd;i<=wd;++i) dp[i]=0;
for(int i=0;i<=len;++i) f[i]=0;
}
int main(){
n=read();m=read();nm=n*m;
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j){
char c=getchar();
if(c<48||c>49) c=getchar();
if(c=='1') ++s[i];
}
for(int i=1;i<=n;++i) a[i]=read();
for(int i=0;i<=m;++i){
c[i][0]=1;
for(int j=1;j<=i;++j){
c[i][j]=c[i-1][j]+c[i-1][j-1];
if(c[i][j]>=P) c[i][j]-=P;
}
}
inv[1]=1;
for(int i=2;i<=(nm<<1);++i) inv[i]=(ll)inv[P%i]*(P-P/i)%P;
solve();
int f0=res0,f1=res1;
for(int i=1;i<=n;++i) s[i]=a[i];
solve();
int g0=res0,g1=res1;
int cur=((ll)f1*g0-(ll)g1*f0)%P*qp((ll)g0*g0%P)%P;
if(cur<0) cur+=P;
printf("%d\n",cur);
return 0;
}
ARC135 E Sequence of Multiples
当时以为是难题,现在发现 E<D……
发现最后这个序列很容易变成等差数列。找找原因?原来设当前的数为 \(ix\),那么当 \(i\geq x\) 时下一个数一定是 \((i+1)x\)。
那么如果 \(i<x\) 呢?发现下一个数就是 \(ix+x\bmod (i+1)+(i+1)[i+1|x]\),考虑把这个东西表示成 \((i+1)t\) 的形式,发现 \(t\) 就是 \(x-\lfloor \frac{x-1}{i+1}\rfloor\)。
那么同数论分块,猜测这个地板除的取值不会很多,跑个类似整除分块的东西就完了。统计答案需要用一下平方和公式。
这玩意由于 \(x\) 在快速减小所以说是跑不到根号级别的,事实上跑得非常快。
Code
#include <cstdio>
#include <unordered_set>
#include <cassert>
using namespace std;
typedef long long ll;
ll read(){
char c=getchar();ll x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int P=998244353;
template<int X>
constexpr int inv = (ll)inv<P%X>*(P-P/X)%P;
template<>
constexpr int inv<1> = 1;
int S2(ll x){x%=P;return x*(x+1)%P*(x<<1|1)%P*inv<6>%P;}
int S1(ll x){x%=P;return (x*(x+1)>>1)%P;}
void solve(){
ll n=read(),x=read();
ll res=0;
for(ll l=2,r;l<=n;l=r+1){
ll t=(x-1)/l;
if(!t) r=n;
else{
r=(x-1-l*t)/(t<<1)+l;
if(r>n) r=n;
}
res=(res-t%P*S2(r-l))%P;
res=(res+(x-(l-1)*t)%P*S1(r-l))%P;
res=(res+(ll)(x%P)*((l-1)%P)%P*((r-l+1)%P))%P;
if(res<0) res+=P;
x-=(r-l+1)*t;
}
res=(res+x%P*(n%P))%P;
printf("%lld\n",res);
}
int main(){
int tc=read();
while(tc--) solve();
return 0;
}
ARC135 D Add to Square
好像打过这场,但当时因为太菜了甚至没搞懂这道题。
现在回想起了题解做法,发现一行/一列的交错和都是不变量,答案显然有一个下界就是行和列交错和的绝对值之和的较大值,不妨猜测这一定是可以达到的。
然后乱构一下就可以了。具体可以看代码。
Code
#include <cstdio>
#include <algorithm>
using namespace std;
int read(){
char c=getchar();int x=0;bool f=0;
while(c<48||c>57) f|=(c=='-'),c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
if(f) return -x;
return x;
}
const int N=503;
typedef long long ll;
int n,m;
ll a[N][N],b[N],c[N];
void opt(int x,int y,ll t){a[x][y]+=t;b[x]-=t;c[y]-=t;}
int main(){
n=read();m=read();
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j){
a[i][j]=read();
if((i^j)&1) a[i][j]=-a[i][j];
b[i]+=a[i][j];
c[j]+=a[i][j];
a[i][j]=0;
}
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j){
if(b[i]>0&&c[j]>0) opt(i,j,min(b[i],c[j]));
if(b[i]<0&&c[j]<0) opt(i,j,max(b[i],c[j]));
}
for(int i=1;i<=n;++i)
for(int j=i+1;j<=n;++j){
if(b[i]<0&&b[j]>0){
ll t=min(-b[i],b[j]);
opt(i,1,-t);
opt(j,1,t);
}
if(b[i]>0&&b[j]<0){
ll t=min(b[i],-b[j]);
opt(i,1,t);
opt(j,1,-t);
}
}
for(int i=1;i<=m;++i)
for(int j=i+1;j<=m;++j){
if(c[i]<0&&c[j]>0){
ll t=min(-c[i],c[j]);
opt(1,i,-t);
opt(1,j,t);
}
if(c[i]>0&&c[j]<0){
ll t=min(c[i],-c[j]);
opt(1,i,t);
opt(1,j,-t);
}
}
/*
* for(int i=1;i<=n;++i) printf("%lld ",b[i]);
* putchar('\n');
* for(int i=1;i<=m;++i) printf("%lld ",c[i]);
* putchar('\n');
*/
ll res=0;
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j){
if((i^j)&1) a[i][j]=-a[i][j];
res+=abs(a[i][j]);
}
printf("%lld\n",res);
for(int i=1;i<=n;++i){
for(int j=1;j<=m;++j) printf("%lld ",a[i][j]);
putchar('\n');
}
return 0;
}
ARC134 F Flipping Coins
What a problem!
自己的想法是对每一个置换环考虑然后再 exp 起来,发现对于每一个置换环都需要考虑一个类似上升子段的问题,每跳一次置换环跟着置换环的方向决定这条边是否操作,列出 DP 式子做了很久没有成果。
但上升子段这一步其实转化对了,但不用对于单个置换环考虑。我们直接将所有置换环从高往低按顺序拼起来发现不会产生任何新的上升子段。
然后题意就转化成了求一个排列有多少个长度为奇数的极长上升子段。感觉跟一个置换环的问题同样难做啊!
所以最神的一步在这:将排列的上升子段拆成类似 \(\texttt{(ABABC)(ABAB)(ABABC)}\) 这种样子,那么奇数上升子段的个数就是 \(\texttt{C}\) 的个数。
发现 \(\texttt{B}\) 对后面的数没有限制。重新分下组,每遇到一个 \(\texttt{B}\) 就分一组。这样把每一组的 EGF 卷起来就是原排列的 EGF 了。发现除了最后一组只有 \(\texttt{C}\) 以外其它的组的 EGF 都是一样的,每次考虑加入一个第一段,列出方程 \(F=FG+H\),这就是一个求逆的形式。于是 \(O(n\log n)\) 做完了。
Code
#include <cstdio>
#include <algorithm>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int P=998244353,N=1<<20;
typedef long long ll;
int qp(int a,int b=P-2){
int res=1;
while(b){
if(b&1) res=(ll)res*a%P;
a=(ll)a*a%P;b>>=1;
}
return res;
}
int rev[N],cw[N+1];
int len,ilen;
void init(int _len){
len=1;int bt=-1;
while(len<_len) len<<=1,++bt;
cw[len]=cw[0]=1;
int w=qp(3,(P-1)>>(bt+1));
ilen=qp(len);
for(int i=1;i<len;++i){
rev[i]=(rev[i>>1]>>1)|((i&1)<<bt);
cw[i]=(ll)cw[i-1]*w%P;
}
}
void NTT(int *arr){
for(int i=0;i<len;++i)
if(i<rev[i]) swap(arr[i],arr[rev[i]]);
for(int i=1,t=1;i<len;i<<=1,++t)
for(int j=0;j<len;j+=(i<<1))
for(int k=j,tt=0;k<(j|i);++k,tt+=(len>>t)){
int x=arr[k],y=(ll)arr[k|i]*cw[tt]%P;
if((arr[k]=x+y)>=P) arr[k]-=P;
if((arr[k|i]=x-y)<0) arr[k|i]+=P;
}
}
void INTT(int *arr){
for(int i=0;i<len;++i)
if(i<rev[i]) swap(arr[i],arr[rev[i]]);
for(int i=1,t=1;i<len;i<<=1,++t)
for(int j=0;j<len;j+=(i<<1))
for(int k=j,tt=len;k<(j|i);++k,tt-=(len>>t)){
int x=arr[k],y=(ll)arr[k|i]*cw[tt]%P;
if((arr[k]=x+y)>=P) arr[k]-=P;
if((arr[k|i]=x-y)<0) arr[k|i]+=P;
}
for(int i=0;i<len;++i) arr[i]=(ll)arr[i]*ilen%P;
}
int tmp[N];
void INV(int *arr,int *res,int _len){
res[0]=qp(arr[0]);
for(int t=1;t<_len;t<<=1){
init(t<<2);
for(int i=0;i<(t<<1);++i) tmp[i]=arr[i];
for(int i=(t<<1);i<len;++i) tmp[i]=0;
NTT(res);NTT(tmp);
for(int i=0;i<len;++i){
res[i]=(2-(ll)tmp[i]*res[i])%P*res[i]%P;
if(res[i]<0) res[i]+=P;
}
INTT(res);
for(int i=(t<<1);i<len;++i) res[i]=0;
}
for(int i=_len;i<len;++i) res[i]=0;
}
int fac[N],fiv[N];
int F[N],G[N];
int n,m,lim,cur,res;
void initfac(int lim){
fac[0]=1;
for(int i=1;i<=lim;++i) fac[i]=(ll)fac[i-1]*i%P;
fiv[lim]=qp(fac[lim]);
for(int i=lim;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
}
int main(){
n=read();m=read();initfac(n);
F[0]=1;
for(int i=2,w=1;i<=n;++i,w=(ll)w*m%P) F[i]=(ll)fiv[i]*(P-i+1)%P*w%P;
INV(F,G,n+1);
int res=0;
for(int i=0,w=1;i<=n;++i,w=(ll)w*m%P) res=(res+(ll)fiv[i]*w%P*G[n-i])%P;
res=(ll)res*fac[n]%P;
printf("%d\n",res);
return 0;
}
ARC134 E Modulo Nim
爆搜题?爆搜的时候需要排序去重然后去掉 0(容易发现这样的胜负态不变)。打个表发现必败的状态很少,手玩一些必败态的性质:
-
全 1 全 2 肯定必败。
-
如果有奇数的话(除了全 1),那么模 2 就成了全 1 所以必胜。因此必败态全是偶数。
-
同理如果有模 4 余 2 的数(除了全 2)的话模个 4 就必胜了。因此必败态全是 4 的倍数。
-
\(\{4,8\}\) 必败,这个手玩一下就行了。所以如果有了比 8 大的数(即 \(\max a_i\geq 12\))的话出现 4,8 就一定必胜了(不考虑全 1 全 2)。因为考虑模 12 意义下如果只有 4/8 的话模 3 就变成全 1 全 2,否则模个 12 就变成 \(\{4,8\}\) 这种状态了。
综上所述,除了三种情况 \(\{1\},\{2\},\{4,8\}\) 外其它的必败态所有数都是 12 的倍数。而 200 内只有 16 个 12 的倍数,直接将 \(2^{16}\) 个状态拿出来爆搜,看起来复杂度没底但 1 秒多就跑完了。
Code
#include <cstdio>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=203,P=998244353;
typedef long long ll;
int n,a[N];
const int f[265]={/* 略去 */};
int g[1<<16],h[1<<16];
void inc(int &x,int v){if((x+=v)>=P) x-=P;}
int main(){
n=read();
for(int i=1;i<=n;++i) a[i]=read();
g[0]=1;
bool fl2=1,fl4=1,fl8=1;
int p=1,all=1;
for(int i=1;i<=n;++i){
for(int s=0;s<(1<<16);++s) h[s]=g[s],g[s]=0;
for(int j=0;(j+1)*12<=a[i];++j)
for(int s=0;s<(1<<16);++s) inc(g[s|(1<<j)],h[s]);
if(a[i]<2) fl2=0;
if(a[i]<4) fl4=0;
if(a[i]<8) fl8=0;
all=(ll)all*a[i]%P;
p=(ll)p*((a[i]>=4)+(a[i]>=8))%P;
}
int res=1+fl2-fl4-fl8+p;
if(res<0) res+=P;
if(res>=P) res-=P;
for(int i=0;i<265;++i) inc(res,g[f[i]]);
all-=res;
if(all<0) all+=P;
printf("%d\n",all);
return 0;
}
Code
#include <map>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
typedef vector<int> vi;
map<vi,bool> mp;
bool dfs(vi s){
if(s.empty()) return 1;
if(mp.find(s)!=mp.end()) return mp[s];
int mx=s.back();
for(int i=1;i<=mx;++i){
vi t;
for(int x:s) if(x%i) t.emplace_back(x%i);
sort(t.begin(),t.end());
t.erase(unique(t.begin(),t.end()),t.end());
if(!dfs(t)) return mp[s]=1;
}
return mp[s]=0;
}
bool f[1<<16];
int main(){
for(int s=0;s<(1<<16);++s){
vi seq;
for(int i=0;i<16;++i)
if(s>>i&1) seq.emplace_back((i+1)*12);
f[s]=dfs(seq);
}
return 0;
}
ARC133 E Cyclic Medians
没有思维,被两年前 zhy 乱杀了 QwQ……
首先遇到这种题,第一部是转化成算 \(\geq x\) 的概率转成 \(01\) 序列,考虑倒序枚举每一个元素对,然后钦定它是最后一个出现的 \((1,1)\) 且后面没有 \((0,0)\)。然后我的做法是拿并查集并并并然后算算算,这样无论如何都很难避免 \(O(n^2)\) 的复杂度。
但是发现最后一个出现的是 \((1,1)\) 的方案数与最后一个出现的是 \((0,0)\) 的方案数成一定比例关系。当 \(x=p\) 与 \(x=V-p\) 这两个方案数正好相等,所以我们只要考虑算出只出现了 \((0,1)\) 的方案数,减一减再除以二就是 \((1,1)\) 的贡献了。
Code
#include <cstdio>
#include <algorithm>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=400003,P=998244353;
typedef long long ll;
int gcd(int a,int b){
if(!b) return a;
return gcd(b,a%b);
}
int qp(int a,int b=P-2){
int res=1;
while(b){
if(b&1) res=(ll)res*a%P;
a=(ll)a*a%P;b>>=1;
}
return res;
}
int n,m,a,g,cur,lim,pw;
int calc(int a,int b){return ((ll)qp(cur,a)*qp(lim-cur,b)+(ll)qp(cur,b)*qp(lim-cur,a))%P;}
int solve(){
int res=pw;
if(a>cur){
res+=qp(calc(n/g,m/g),g);
if(res>=P) res-=P;
}
else{
res-=qp(calc(n/g,m/g),g);
if(res<0) res+=P;
}
if(res&1) res+=P;
res>>=1;
return res;
}
int main(){
n=read();m=read();lim=read();a=read();g=gcd(n,m);
int tmp=pw=qp(lim,n+m);
for(cur=1;cur<lim;++cur){
tmp+=solve();
if(tmp>=P) tmp-=P;
}
printf("%d\n",tmp);
return 0;
}
ARC132 F Takahashi The Strongest
虽然 zhy 讲这场 E>F,但我还是感觉 F>E。
考虑 FWT 可以做什么事情:对于一个以 \(k\) 维向量为下标的数组 \(F_{a}\),我们可以对每一维构造一个线性变换 \(M_i\),那我们就可以简便地计算整体的线性变换,其中 \(a\to b\) 的贡献是 \(\prod_{i=1}^k M_{i,(a_i,b_i)}\)。
这道题中,如果直接对 \(3^n\) 的空间做线性变换是难以处理的。考虑扩一个 \(\texttt{?}\) 做 \(4^n\) 的变换。
我们先将 Aoki 和 Snuke 的数组做一个变换让 \(\texttt{?}\) 代表任意的 \(\texttt{P,R,S}\)。然后点乘起来,此时 \(\texttt{R}\) 其实代表 \(\texttt{(R,R)}\),问号其实代表任何一个二元组。
我们现在想求一个位置除了 \(\texttt{(R,R)}\) 都可以的方案数,那么每一个位置的线性变换直接用 \(\texttt{?}\) 减去 \(\texttt{R}\) 就可以了,其它字符同理。
Code
#include <cstdio>
using namespace std;
typedef long long ll;
int k,n,m,len;
ll f[1<<24],g[1<<24];
int trans(char c){
if(c=='R') return 0;
if(c=='S') return 1;
if(c=='P') return 2;
__builtin_unreachable();
}
char s[15];
int main(){
scanf("%d%d%d",&k,&n,&m);
for(int i=0;i<n;++i){
scanf("%s",s);
int res=0;
for(int t=0;t<k;++t) res=res<<2|trans(s[t]);
++f[res];
}
for(int i=0;i<m;++i){
scanf("%s",s);
int res=0;
for(int t=0;t<k;++t) res=res<<2|trans(s[t]);
++g[res];
}
len=(1<<(k<<1));
for(int p=1;p<len;p<<=2)
for(int i=0;i<len;i+=(p<<2))
for(int t=i;t<i+p;++t){
f[t+p+p+p]+=f[t]+f[t+p]+f[t+p+p];
g[t+p+p+p]+=g[t]+g[t+p]+g[t+p+p];
}
for(int i=0;i<len;++i) f[i]*=g[i];
for(int p=1;p<len;p<<=2)
for(int i=0;i<len;i+=(p<<2))
for(int t=i;t<i+p;++t){
ll sm=f[t+p+p+p];
f[t]=sm-f[t];
f[t+p]=sm-f[t+p];
f[t+p+p]=sm-f[t+p+p];
}
for(int i=0;i<len;++i){
bool fl=0;
for(int j=0;j<k;++j)
if(((i>>j)>>j&3)==3){fl=1;break;}
if(fl) continue;
printf("%lld\n",(ll)n*m-f[i]);
}
return 0;
}
ARC132 E Paw
结论有意思但比较水。
设有 \(m\) 个洞相当于让我们决定一个长度为 \(m\) 的排列表示删除时间和一个 \(\texttt{<>}\) 串表示删除时走的方向。
手玩几下发现最终的操作数组一定会变成 \(\dots\texttt{<<<xx}\dots \texttt{xx>>>}\dots\) 这种形式,其中 \(\texttt{x}\) 代表原序列。进一步地,我们发现最后的一段 \(\texttt{<<<xx}\dots \texttt{xx>>>}\) 一定正好是相邻两个洞之间的元素。
那么就可以枚举每对相邻的洞计算成为答案的概率。考虑这一段 \(\texttt{<<<xx}\dots \texttt{xx>>>}\) 什么时候有贡献,那么就是将排列按照这个位置劈开,前缀的所有后缀最大值的位置都是 \(\texttt{>}\),后缀的所有前缀最大值的位置都是 \(\texttt{<}\)。前缀最大值/后缀最大值考虑经典的插入一个排列算贡献,大概是 \((2n-1)!!\) 这样的形式。
做完了。
Code
#include <cstdio>
using namespace std;
const int N=100003,P=998244353;
typedef long long ll;
int n,m;
char s[N];
int pre[N],pos[N],t[N],fac[N],fiv[N];
int qp(int a,int b=P-2){
int res=1;
while(b){
if(b&1) res=(ll)res*a%P;
a=(ll)a*a%P;b>>=1;
}
return res;
}
int C(int a,int b){return (ll)fac[a]*fiv[b]%P*fiv[a-b]%P;}
int main(){
scanf("%d%s",&n,s+1);
for(int i=1;i<=n;++i){
if(s[i]=='.') pos[++m]=i;
t[i]=t[i-1]+(s[i]=='<');
}
pos[0]=0;pos[m+1]=n+1;
pre[0]=1;
for(int i=1;i<=m;++i) pre[i]=(ll)pre[i-1]*(i+i-1)%P;
fac[0]=1;
for(int i=1;i<=m;++i) fac[i]=(ll)fac[i-1]*i%P;
fiv[m]=qp(fac[m]);
for(int i=m;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
int res=0;
for(int i=0;i<=m;++i)
res=(res+(ll)(t[pos[i+1]-1]-t[pos[i]]+pos[i])%P*pre[i]%P*pre[m-i]%P*C(m,i))%P;
res=(ll)res*fiv[m]%P;
for(int i=1;i<=m;++i){
if(res&1) res+=P;
res>>=1;
}
printf("%d\n",res);
return 0;
}
ARC131 F ARC Stamp
不应该看 Sol 的,其实可以自己做出来。感觉比前面的铜牌简单。
第一步感觉很像很久以前某次联考的一道题,时光倒流后就变成了将 \(\texttt{ARC}\) 变成 \(\texttt{???}\)。观察操作顺序形如将一段 \(\texttt{ARC}\) 变成问号后再不断的往左右扩展 \(\texttt{AR/A/RC/C}\)。其中如果两个 \(\texttt{?}\) 中夹了个 \(\texttt{R}\) 也是可以操作的。
那么将序列按照上述操作过程分段。相当于每一段以 \(\texttt{ARC}\) 为中心扩展到两边的连续段都可以选一段包括中间 \(\texttt{ARC}\) 的区间点亮,要求点亮不超过 \(k\) 个段。注意对于两个连续段夹着一个 \(\texttt{R}\) 的情况,那么这个区间可以跨过这个 \(\texttt{R}\)。
直接考虑对这个东西 DP 计数。发现如果直接按 \(3\) 的问号个数次方算的话会算重一些。那么我们就规定对于每一个连续段中点亮的区间两端的段都必须与原串不同,与原串相同的地方直接从两头往里缩总可以缩到这一种情况。注意对于每一个连续段间的 \(\texttt{R}\) 都可以选择直接不点亮,所以 \(\texttt{R}\) 替换成的字符要不是 \(\texttt{R}\)。
于是这道题做完了。
Code
#include <cstdio>
#include <cstring>
using namespace std;
const int N=5003,P=998244353;
typedef long long ll;
char s[N];
int n,k,m;
int f[N][N][2];
int op[N<<1];
// 0->X
// 1->ARC
// 2->AR
// 3->A
// 4->RC
// 5->C
// 6->R
int stk[N],tp;
void inc(int &x,int v,int p=1){
if(p==1){x+=v;if(x>=P) x-=P;}
else x=(x+(ll)v*p)%P;
}
int main(){
scanf("%s%d",s+1,&k);
n=strlen(s+1);
int las=-1;
for(int i=2;i<=n-1;++i){
if(s[i-1]=='A'&&s[i]=='R'&&s[i+1]=='C'){
int l=i-2,r=i+2;
while(l){
if(l>1&&s[l-1]=='A'&&s[l]=='R'){stk[++tp]=2;l-=2;continue;}
if(s[l]=='A'){stk[++tp]=3;--l;continue;}
break;
}
if(~las){
if(s[l]=='R'&&las==l) op[++m]=6;
else op[++m]=0;
}
while(tp) op[++m]=stk[tp--];
op[++m]=1;
while(r<=n){
if(r<n&&s[r]=='R'&&s[r+1]=='C'){op[++m]=4;r+=2;continue;}
if(s[r]=='C'){op[++m]=5;++r;continue;}
break;
}
las=r;
}
}
if(k>m) k=m;
f[0][0][0]=1;
for(int i=1;i<=m;++i)
for(int j=0;j<i&&j<=k;++j){
int f0=f[i-1][j][0],f1=f[i-1][j][1];
switch(op[i]){
case 0:
inc(f[i][j][0],f0);
break;
case 1:
inc(f[i][j][0],f0);
inc(f[i][j+1][0],f0,26);
inc(f[i][j+1][1],f0,27);
inc(f[i][j+1][0],f1,27);
inc(f[i][j+1][1],f1,27);
break;
case 2:
inc(f[i][j][0],f0);
inc(f[i][j+1][1],f0,8);
inc(f[i][j+1][1],f1,9);
break;
case 3:
inc(f[i][j][0],f0);
inc(f[i][j+1][1],f0,2);
inc(f[i][j+1][1],f1,3);
break;
case 4:
inc(f[i][j][0],f0);
inc(f[i][j+1][0],f1,8);
inc(f[i][j+1][1],f1,9);
break;
case 5:
inc(f[i][j][0],f0);
inc(f[i][j+1][0],f1,2);
inc(f[i][j+1][1],f1,3);
break;
case 6:
inc(f[i][j][0],f0);
inc(f[i][j+1][1],f1,2);
break;
default:
__builtin_unreachable();
}
}
int res=0;
for(int i=0;i<=k&&i<=m;++i) inc(res,f[m][i][0]);
printf("%d\n",res);
return 0;
}
ARC130 F Replace by Average
有意思但没想象的那么难。
如果没有下取整号的话,操作无限次后答案显然趋近于点集的下凸包。有了取整符号我一开始的尝试是每次将凸包强行下取整然后再跑凸包迭代。不幸的是,由于庞大的值域这种方法会迭代过多次。
理解一下官方题解在说什么。首先所有操作过程中,\(\forall p\in \Z,c_p=\min_{i=1}^n\{a_i-pi\}\) 永远是一个不变量。由于 \(p\) 是整数,所以任意一次操作都不会减小这个值。
我们就得到了最终序列 \(a'\) 的一个下界 \(a'_i\geq \max_{i=1}^n \{c_p+pi\}\),同时我们也容易发现这个下界是可以达到的,因为考虑一个 \(p\) 使得 \(\min_{i=1}^n\{a_i-pi\}=\min_{i=1}^n\{a'_i-pi\}\) 在 \(i\) 处取最值,那么 \(a'_i\) 就取到了 \(c_p+pi\),而由于答案序列凸性,这样的 \(p\) 一定是存在的。
那么我们只需要求出对于每个点 \(i\),有哪些 \(p\) 满足一开始时在这个点取到最小值(这一定是一段区间,所以我们只需要取最小的与最大的 \(p\) 出来跑),然后将 \(a'_i\) 置为所有 \(c_p+pi\) 的最大值。
具体的实现方法是求出 \(a\) 的凸包后,将相邻的直线下取整/上取整就得到了那些 \(p\) 在这个点取到最小值,将这些直线再跑一遍凸包就行了。
接下来讲讲自己的理解:
考虑“凸包”的意义是什么。可以建立在对于线性规划的理解上,对于一个线性最优化的方向 \(\vec{v}\),一个点的价值定义为与该方向的点积,而由点积的组合意义可以将这个东西看成用一条与 \(\vec{v}\) 垂直的过原点的线开始沿着 \(\vec{v}\) 扫,直到碰到这个点时扫过的距离。
凸包的求法和单调栈为什么很类似?不在单调栈的节点意味着存在一个节点比它后还比它优,也就是二维偏序了它。偏序有另一种理解方式:这个点无论在什么样的最优化目标下都不起作用了!而不在凸包上的点也有类似的理解:无论选取什么样的线性最优化方向 \(\vec{v}\),这个点都被偏序,也就是不可能成为最优值了!
那么什么是下凸包呢?就是限制了 \(\vec{v}\) 在 \(y\) 轴上的分量是非负的,求出来的凸包就是下凸包了!
这样引申出凸包的另一个直观理解,拿一根无限长的棍子从任意一个最优化方向 \(\vec{v}\) 接近这个点集直到抵到第一个点,然后开始连续旋转一整圈,旋转过程中没有被扫过的点就是在凸包的内部。
如果求上/下凸包,那么就只用转半圈。
这道题又是什么呢?它让我们求一个下凸包还是整数凸包。也就是说不仅限制了 \(\vec{v}\) 的方向,还限制了它是一个整数向量!
那么它的求法就是拿一根无限长的棍子竖直地从左向右接近这个点集直到抵到第一个点,然后开始离散旋转半圈,旋转过程中所有时刻棍子的另一边的半平面交就是下半的整数凸包了。
Code
#include <cstdio>
#include <cmath>
using namespace std;
template<typename T>
T read(){
char c=getchar();T x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
typedef long long ll;
const int N=300003,T=600003;
int n;
ll a[N],b[N];
int stk[N],tp;
bool judge(int x,int y,int z){
return (a[y]-a[x])*(z-y)>=(a[z]-a[y])*(y-x);
}
ll lk[T],lb[T];int rk;
bool check(int x,int y,int z){
return (__int128)(lb[x]-lb[y])*(lk[z]-lk[y])>=(__int128)(lb[y]-lb[z])*(lk[y]-lk[x]);
}
bool condi(int x,int y,int p){return (lb[x]-lb[y])<p*(lk[y]-lk[x]);}
int main(){
n=read<int>();
for(int i=1;i<=n;++i) a[i]=read<ll>();
for(int i=1;i<=n;++i){
while(tp>1&&judge(stk[tp-1],stk[tp],i)) --tp;
stk[++tp]=i;
}
for(int i=1;i<=tp;++i){
ll uk=-1e18,vk=1e18;
if(i>1) uk=ceil((double)(a[stk[i]]-a[stk[i-1]])/(stk[i]-stk[i-1]));
if(i<tp) vk=floor((double)(a[stk[i+1]]-a[stk[i]])/(stk[i+1]-stk[i]));
if(uk>vk) continue;
if(i>1&&(!rk||lk[rk]!=uk)){++rk;lb[rk]=a[stk[i]]-(lk[rk]=uk)*stk[i];}
if(i<tp&&(!rk||lk[rk]!=vk)){++rk;lb[rk]=a[stk[i]]-(lk[rk]=vk)*stk[i];}
}
tp=0;
for(int i=1;i<=rk;++i){
while(tp>1&&check(stk[tp-1],stk[tp],i)) --tp;
stk[++tp]=i;
}
ll res=0;
for(int i=1,p=1;i<=n;++i){
while(p<tp&&condi(stk[p],stk[p+1],i)) ++p;
res+=lk[stk[p]]*i+lb[stk[p]];
}
printf("%lld\n",res);
return 0;
}
ARC130 E Increasing Minimum
有意思但不难。容易发现题目需要我们将序列划分成 \(k\) 个区间,满足每个区间元素不重,设其第 \(i\) 个区间元素组成的集合为 \(S_i\),还要满足 \(S_1\subseteq S_2\subseteq S_3\dots \subseteq S_k\)。
然而这个结论假了,因为最后一段可能不满足这个限制,可以只出现一个子集,所以我们单独挖去一个最后一段,考虑每一个前缀能不能划分。这个可以用 DP 维护,每个点可以转移到一段区间,用差分做就行了。
剩下的就是字典序最小的限制。发现我们在保证了所有元素出现次数的最大值一定时,选取的后缀只需要尽可能小就行了。于是只有两种可能的答案,比较一下就可以了。
Code
#include <cstdio>
#include <vector>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=300003;
int n,k,a[N],c[N],s[N],d[N];
int f[N],nxt[N],las[N];
bool dp[N],vis[N];
vector<int> res;
int main(){
n=read();k=read();
for(int i=1;i<=k;++i) ++c[a[i]=read()];
for(int i=1;i<=n;++i) las[i]=k+1;
for(int i=k;i;--i){
nxt[i]=las[a[i]];
las[a[i]]=i;
}
for(int i=1;i<=k;++i) ++s[nxt[i]];
dp[0]=1;
for(int i=0,cur=1,p=0;i<=k;++i){
if(i){
d[i]+=d[i-1];
--s[nxt[i]];
if(nxt[i]>cur) cur=nxt[i];
}
if(d[i]) dp[i]=1;
while(p<=k&&!s[p]) ++p;
if(dp[i]&&cur<p){++d[cur];--d[p];}
}
int mx=0,cc=0;
for(int i=1;i<=n;++i){
if(c[i]>mx) mx=c[i],cc=0;
if(mx==c[i]) ++cc;
}
res=vector<int>(1,0x3f3f3f3f);
bool f0=0,f1=0;
for(int i=k;~i;--i){
if(dp[i]){
if(cc){
if(!f1){
vector<int> cur(n);
f1=1;
for(int t=1;t<=n;++t) cur[t-1]=mx-c[t]+1;
if(res>cur) res=cur;
}
}
else{
if(!f0){
vector<int> cur(n);
f0=1;
for(int t=1;t<=n;++t) cur[t-1]=mx-c[t];
if(res>cur) res=cur;
}
}
}
if(vis[a[i]]) break;
vis[a[i]]=1;
if(c[a[i]]==mx) --cc;
--c[a[i]];
}
if(res[0]==0x3f3f3f3f) puts("-1");
else{
for(int i=0;i<n;++i) printf("%d ",res[i]);
putchar('\n');
}
return 0;
}
ARC129 F Let's Play Tag
前面的部分其实是一眼题,手动模拟一下发现每一次转弯,当前花的时间需要乘上 3 倍然后加上两个端点的距离。也就是说假设转弯序列的位置是 \(L_1,R_1,L_2,R_2\dots L_k,R_k\),那么贡献是 \(R_k+4L_k+4\times 3L_{k-1}+4\times 3^2R_{k-1}\dots\) 这样的形式。
然后剩下的 GF 部分就是不会不会不会……接下来又 orz zjk。
考虑我们只算出 \(L\) 的一个段对答案贡献,考虑 \(R\) 对方案数的贡献只跟 \(L\) 分了几段有关,所以我们考虑对“分了几段”这个东西求出 GF。
枚举 \(L\) 每个点 \(t\) 作为段尾(最后一个点的贡献有点特殊,我们单独算),除去一些常数倍的贡献外,剩下的贡献取决于它后面被分了几段,就乘上几个 \(9\),即 \((1+9x)^{n-t-1}\)。前面对于段数的贡献是 \((1+x)^{t-1}\)。注意这里 GF 没有算自己这个段和最后那个段,最后答案要乘上 \(x^2\)。我们现在主要是要求:
我自己做时推出过这个 GF,但当时认为很不可做于是去想组合意义去了。
然而这个东西其实是可以直接做的,观察发现这个 GF 很像等比数列求和,所以我们可以分治用求矩阵等比数列和的类似方法解决这个问题。复杂度 \(O(n\log^2 n)\)。
当然也有更好的做法。需要一定的直觉与判断力。注意到 GF 中两个指数之和是一个常数,如果有一个底数形式简单(比如一个单项式 \(x\))就可以大力二项式定理化简。我们换元 \(p=x+1\),这样有:
后面的组合数可以拆开成减法卷积的形式,于是一遍卷就可以把 GF 化简成 \(\sum_{i=0}^{n-2} a_i(x+1)^i\) 这种形式。再用二项式定理拆开然后减法卷积就可以得到原 GF。
注意到每一次卷都是卷同一个多项式 \(e^{x^{-1}}=\sum_{i=0}^{\infin} \frac{1}{i!} x^{-i}\),可以预处理它的 DFT 结果,总共只需要 9 次 NTT,常数比较小。复杂度 \(O(n\log n)\)。
Code
#include <cstdio>
#include <algorithm>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=250003,P=998244353;
const int I9=443664157,N89=443664156;
typedef long long ll;
int n,m,lim,res;
int a[N],b[N];
int rev[1<<19],cw[1<<19|1];
int len,ilen;
int f[1<<19],g[1<<19];
int fac[N],fiv[N];
int qp(int a,int b=P-2){
int res=1;
while(b){
if(b&1) res=(ll)res*a%P;
a=(ll)a*a%P;b>>=1;
}
return res;
}
void init(int _len){
len=1;int bt=-1;
while(len<_len) len<<=1,++bt;
cw[len]=cw[0]=1;
int w=qp(3,(P-1)>>(bt+1));
ilen=qp(len);
for(int i=1;i<len;++i){
rev[i]=(rev[i>>1]>>1)|((i&1)<<bt);
cw[i]=(ll)cw[i-1]*w%P;
}
}
void NTT(int *arr){
for(int i=0;i<len;++i)
if(i<rev[i]) swap(arr[i],arr[rev[i]]);
for(int i=1,t=1;i<len;i<<=1,++t)
for(int j=0;j<len;j+=(i<<1))
for(int k=j,tt=0;k<(j|i);++k,tt+=(len>>t)){
int x=arr[k],y=(ll)arr[k|i]*cw[tt]%P;
if((arr[k]=x+y)>=P) arr[k]-=P;
if((arr[k|i]=x-y)<0) arr[k|i]+=P;
}
}
void INTT(int *arr){
for(int i=0;i<len;++i)
if(i<rev[i]) swap(arr[i],arr[rev[i]]);
for(int i=1,t=1;i<len;i<<=1,++t)
for(int j=0;j<len;j+=(i<<1))
for(int k=j,tt=len;k<(j|i);++k,tt-=(len>>t)){
int x=arr[k],y=(ll)arr[k|i]*cw[tt]%P;
if((arr[k]=x+y)>=P) arr[k]-=P;
if((arr[k|i]=x-y)<0) arr[k|i]+=P;
}
for(int i=0;i<len;++i) arr[i]=(ll)arr[i]*ilen%P;
}
int C(int a,int b){
if(b<0||a<b) return 0;
return (ll)fac[a]*fiv[b]%P*fiv[a-b]%P;
}
void solve(){
for(int i=0;i<len;++i) f[i]=0;
for(int i=1,pw=1;i<n;++i){
pw=(ll)pw*I9%P;
f[n-1-i]=(ll)fac[n-1-i]*pw%P*a[i]%P;
}
NTT(f);
for(int i=0;i<len;++i) f[i]=(ll)f[i]*g[i]%P;
INTT(f);
for(int i=0,pw=qp(9,n-1);i<n-1;++i){
f[i]=(ll)f[i+lim]*fiv[i]%P*pw%P;
pw=(ll)pw*N89%P;
}
reverse(f,f+n-1);
for(int i=n-1;i<len;++i) f[i]=0;
for(int i=0;i<n-1;++i) f[i]=(ll)f[i]*fac[i]%P;
NTT(f);
for(int i=0;i<len;++i) f[i]=(ll)f[i]*g[i]%P;
INTT(f);
for(int i=0;i<n-1;++i)
res=(res+(C(m-1,i)*12ll+C(m-1,i+1)*48ll+C(m-1,i+2)*36ll)%P*f[i+lim]%P*fiv[i])%P;
for(int i=0;i<n;++i)
res=(res+(C(m-1,i-1)+C(m-1,i)*5ll+C(m-1,i+1)*4ll)%P*a[n]%P*C(n-1,i))%P;
}
int main(){
n=read();m=read();lim=max(n,m);
init(lim<<1|1);
fac[0]=1;
for(int i=1;i<=lim;++i) fac[i]=(ll)fac[i-1]*i%P;
fiv[lim]=qp(fac[lim]);
for(int i=lim;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
for(int i=0;i<=lim;++i) g[i]=fiv[lim-i];
NTT(g);
for(int i=1;i<=n;++i) a[i]=read();
for(int i=1;i<=m;++i) b[i]=read();
solve();
swap(n,m);
for(int i=1;i<=lim;++i) swap(a[i],b[i]);
solve();
printf("%d\n",res);
return 0;
}
ARC129 E Yet Another Minimization
广义切糕一个基础应用?这里来讲讲广义切糕模型。
切糕模型:
有 \(n\) 个变量 \(x_i\in [1,m]\),第 \(i\) 个变量取 \(j\) 时需要花费代价 \(c_{i,j}\),还有若干条形如 \(x_{u_i}\leq x_{v_i}+w_i\) 这样的限制。
对于这个问题可以用最小割建模。对变量 \(i\) 新建 \(m+1\) 个点 \((i,1),(i,2),\dots (i,m+1)\),\((i,j)\) 个点向 \((i,j+1)\) 连流量为 \(c_{i,j}\) 的边。然后对于每条限制连 \(\forall x\in [1,m+1-w_i],(u_i,x+w_i)\to (v_i,x)\) 的一条无穷流量边。
这样建图的正确性这里不赘述,详见 dengyaotriangle 切糕一题的题解。我们可以说明这样建图每一条链都恰好会被割一条边。
然而当限制的形式变成 \(x_{u_i}\geq x_{v_i}+w_i\) 这样的限制时,仿照上面的建图方式会出错,每一条链可能会被割掉多条边。
如何避免?我们只需要建好 \((i,j+1)\to (i,j)\) 的一条无穷流量边就可以了。切糕模型从这里开始变得极为强大,这样的一条回流边很严格地保证了每一条链都恰好会被割一条边。
这是因为有如下显然的事实:
任意最小割的割边 \((u,v)\) 必须满足 \(S\to u,v\to T\)。(否则不割这条边依然是一组割)
这样如果一条链上有两条割边 \((i,x-1)\to (i,x)\),\((i,y)\to (i,y+1)\) 满足 \(x<y\),那么 \(x\to T\),\(S\to y\)。又由于存在回流边 \(y\to x\),于是 \(S\to T\) 与这是一组割矛盾。
回到这道题。本题“限制”也是带权的,我们对 \((a,t)\leftrightarrow (b,t)\) 建一条流量 \(w_{a,b}\) 的边。这样如果 \(a,b\) 分别取 \(x,y(x<y)\),那么这两个点之间所有的边都会被割掉,割掉的流量正好是 \((y-x)w_{a,b}\)。
然后我们将这个图缩点,一个点对应原图上的一个区间,跨链的边的流量就设置为 \(w\) 乘上两个区间交的长度即可。
复杂度 \(O(\text{maxflow}(nm,n^2m))\)。
Code
#include <cstdio>
#include <algorithm>
using namespace std;
template<typename T>
T read(){
char c=getchar();T x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
int n,m,s,t,cnt;
typedef long long ll;
const ll INF=0x3f3f3f3f3f3f3f3f;
namespace net{
const int N=5003,M=1000003;
int hd[N],ver[M],nxt[M],tot=1;
ll val[M];
void add(int u,int v,ll w,ll _w=0){
nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v;val[tot]=w;
nxt[++tot]=hd[v];hd[v]=tot;ver[tot]=u;val[tot]=_w;
}
int dis[N],que[N],tl;
int cur[N];
bool bfs(){
for(int i=1;i<=cnt;++i) dis[i]=-1,cur[i]=hd[i];
dis[que[tl=1]=s]=0;
for(int pos=1;pos<=tl;++pos){
int u=que[pos];
for(int i=hd[u];i;i=nxt[i]){
if(!val[i]) continue;
int v=ver[i];
if(~dis[v]) continue;
dis[que[++tl]=v]=dis[u]+1;
}
}
return ~dis[t];
}
ll dfs(int u,ll fl){
if(u==t||!fl) return fl;
ll sm=0;
for(int i=cur[u];i&&fl;i=nxt[i]){
cur[u]=i;
if(!val[i]) continue;
int v=ver[i];
if(dis[v]!=dis[u]+1) continue;
ll t=dfs(v,min(fl,val[i]));
fl-=t;val[i]-=t;val[i^1]+=t;sm+=t;
}
return sm;
}
ll flow(){
ll res=0;
while(bfs()) res+=dfs(s,INF);
return res;
}
}
const int N=53,M=7,V=1000000;
int p[N][M];
int f(int i,int j){return (i-1)*(m+1)+j;}
int main(){
n=read<int>();m=read<int>();
cnt=n*(m+1);s=++cnt;t=++cnt;
for(int i=1;i<=n;++i){
net::add(s,f(i,1),INF);
for(int j=1;j<=m;++j){
p[i][j]=read<int>();
net::add(f(i,j),f(i,j+1),read<ll>(),INF);
}
p[i][m+1]=V+1;
net::add(f(i,m+1),t,INF);
}
for(int x=1;x<=n;++x)
for(int y=x+1;y<=n;++y){
int w=read<int>();
int a=1,b=1;
while(a<=m||b<=m){
int l=max(p[x][a-1],p[y][b-1]);
int r=min(p[x][a],p[y][b]);
ll ww=(ll)(r-l)*w;
net::add(f(x,a),f(y,b),ww,ww);
if(p[x][a]<p[y][b]){++a;continue;}
if(p[x][a]>p[y][b]){++b;continue;}
++a;++b;
}
}
printf("%lld\n",net::flow());
return 0;
}
ARC128 F Game against Robot
计数水平还是太低了……orz AppleBlue……
首先这个博弈过程就是稳健型选手的博弈过程。我们考虑转为求出机器人最少拿的权值之和,于是我们按 \(p\) 降序扫描,每加两个 \(\text{pop}\) 一个最小值,\(\text{pop}\) 出来的最小值之和就是答案。
容易发现这道题的入手点在于 \(a_i\) 对答案的贡献只跟它是第几大有关。考虑转 \(01\) 序列做,将前 \(m\) 大看成 \(1\),我们考虑统计 \(\text{pop}\) 出来的 \(1\) 的个数之和,差分一下就是每个数的贡献了。
接下来对于 \(0,1\) 之间可以先除去一个 \(m!(n-m)!\),然后不考虑 \(01\) 内部的顺序。
我们考虑把相邻两次操作看成一个整体,那么有 \(n'=\frac{n}{2}\) 次操作。考虑一个暴力 DP,记录下当前时刻你有几个 \(0\),如果 \(0\) 都没了还 \(\text{pop}\) 那么就要贡献答案。这样,我们获得了一个 \(O(\text{poly}(n))\) 的做法。
考虑记 \((x,y)\) 为当前时刻和当前 \(0\) 的个数,将这 \(n'\) 次操作画成折线:
-
如果是 \((0,0)\),那么转移到 \((x+1,y+1)\),方案数为 \(1\)。
-
如果是 \((0,1),(1,0)\),那么转移到 \((x+1,y)\),方案数为 \(2\)。
-
如果是 \((1,1)\),那么转移到 \((x+1,y-1)\),方案数为 \(1\)。特别地,如果 \(y=0\) 那么转移到 \((x+1,0)\),并且贡献一次答案。
我们发现这个对 \(0\) 取 \(\max\) 的操作太烦了!我们忽略这个操作,考察原折线(显然该折线终点为 \((n',n'-m)\)),发现贡献答案的次数正好是折线纵坐标的最小值的相反数!于是我们要考虑的对象就变成了原折线的最低点。
我们先抛开最低点,如果直接从 \((0,0)\) 走到 \((n',t)\) 的方案数是多少呢?这相当于 \([x^t](x^{-1}+x+2)^{n'}\),即 \({n\choose n'+t}\)。
接下来我们考虑直接算贡献答案的次数 \(\geq k\) 的方案数,发现这相当于钦定必须经过 \(y=-k\) 这一条线的方案数,即 \({n\choose m-2k}\)。由于贡献答案的次数必须 \(\geq \max(0,m-n)\),我们记该值为 \(p\)。将它们累加起来算贡献,就可以得到答案为:
于是这道题做完了!!!
复杂度只有一个 \(\text{sort}\)。
Code
#include <cstdio>
#include <algorithm>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=1000003,P=998244353;
typedef long long ll;
int fac[N],fiv[N];
int qp(int a,int b=P-2){
int res=1;
while(b){
if(b&1) res=(ll)res*a%P;
a=(ll)a*a%P;b>>=1;
}
return res;
}
int n,nn,a[N];
int pre[N];
void inc(int &x,int v){if((x+=v)>=P) x-=P;}
int C(int a,int b){return (ll)fac[a]*fiv[b]%P*fiv[a-b]%P;}
int f[N];
int main(){
nn=read();n=nn>>1;
int sm=0;
for(int i=1;i<=nn;++i) inc(sm,a[i]=read());
sort(a+1,a+nn+1);
fac[0]=1;
for(int i=1;i<=nn;++i) fac[i]=(ll)fac[i-1]*i%P;
fiv[nn]=qp(fac[nn]);
for(int i=nn;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
pre[0]=1;pre[1]=nn;
for(int i=2;i<=nn;++i) inc(pre[i]=pre[i-2],C(nn,i));
for(int i=0;i<=nn;++i){
int mx=max(i-n,0);
if(mx+mx+2<=i) f[i]=pre[i-mx-mx-2];
if(mx+mx<=i) f[i]=(f[i]+(ll)C(nn,i-mx-mx)*mx)%P*fac[nn-i]%P*fac[i]%P;
}
int res=0;
for(int i=nn;i;--i)
res=(res+(ll)a[i]*(f[nn-i+1]-f[nn-i]+P))%P;
if(res) res=P-res;
res=(res+(ll)fac[nn]*sm)%P;
printf("%d\n",res);
return 0;
}
ARC127 F ±AB
由于题目保证了 \(\gcd(a,b)=1\),所以我们容易猜到大多数情况下,答案就是 \(m+1\)。
我们发现当 \(m\geq a+b-1\) 时,任何的数都可以被走到,因为你将每个数都表示成 \(ax+by+v\) 的形式,如果 \(xy\geq 0\) 那么直接操作就做完了,否则我们不断地操作 \(a\) 直到达成目标或不能操作。发现不能操作时一定又能操作一次 \(b\) 了。所以操作有限轮后就可以满足条件了。
然后又只能开题解 QwQ。(怎么总是做不出 F……)
考虑第一次操作确定后,比如下文都在拿 \(+a\) 举例,下一次不能 \(+b\),因为 \(a+b>=m\),也不能 \(-a\),因为会走重。而 \(+a\) 和 \(-b\) 只能有一个能操作。所以后面的操作是固定的。
那么相当于从起点走出了两条链,而且这两条链不会形成任何环,否则这个环上有 \(ax+by=0\),那么此时由于 \(\gcd(a,b)=1\) 所以 \(|x|+|y|>=m\) 矛盾!
相当于这里是要求两条链的链长。观察到链一定会在第一次进入区间 \([m-a+1,b-1]\) 时结束。设此时 \(a\) 操作了 \(p\) 次,那么 \(b\) 一定操作了 \(\lfloor \frac{v+pa}{b} \rfloor\) 次。此时当前位置为:\((v+pa) \bmod b\)。发现当第一次的 \(+a\) 走的出(如果走不出,那么答案时平凡的 \(p=0\)),也就是 \(v\bmod b+a\leq m\) 时,如果有 \(v\bmod b + pa\bmod b\geq b\),那么 \((v+pa) \bmod b\) 会比 \(v\bmod b\) 还小,此时一定还能再进行一次 \(+a\) 矛盾!
记 \(v_0=v\bmod b\) 因此有 \((v+pa) \bmod b=v_0+pa\bmod b\)。所以我们相当于要求 \(pa\bmod b\in [m-a+1-v_0,b-1-v_0]\) (下文记该区间为 \([L,R]\))的最小的 \(p\)。
对于 \(pa\bmod b \in [L,R]\) 的限制,我们考虑用附加的不等式限制拆开 \(\bmod\) 然后递归下去。
我们设 \(pa\in [qb,(q+1)b)\),那么限制相当于 \(pa-qb\in [L,R]\),如果求出了最小的 \(q\),那么 \(p=\lceil \frac{qb+L}{a} \rceil\) 也取到了最小,而且由于 \(L<b\) 满足了 \(pa<(q+1)b\) 的条件。接下来一步想要想到要求很高,将 \(b\) 拆成 \(ka+r\) 的形式。我们转而考虑这个不等式对于 \(r\) 的限制:\(pa-qka-qr\in [L,R]\),前面的部分是 \(a\) 的倍数,我们考虑将 \([L,R]\) 的数都模上 \(a\),如果其中包含了 \(0\) 那么直接取 \(q=0\) 就是最优解了。否则 \(qr\bmod a\) 必然在 \([(-R)\bmod a,(-L)\bmod a]\) 范围内,递归下去就行了。
Code
#include <cstdio>
#include <cassert>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
typedef long long ll;
int f(int a,int b,int pl,int pr){
if(!a) return 0;
if((pl-1)/a!=pr/a) return (pl+a-1)/a;
return ((ll)f(b%a,a,pr?a-pr%a:0,pl?a-pl%a:0)*b+pl+a-1)/a;
}
int calc(int a,int b,int v,int m){
int v_=v%b,k=0;
if(v_+a<=m) k=f(a,b,m-a-v_+1,b-v_-1);
return k+(v+(ll)k*a)/b;
}
void solve(){
int a=read(),b=read(),v=read(),m=read();
if(a+b-1<=m) printf("%d\n",m+1);
else printf("%d\n",calc(a,b,v,m)+calc(b,a,v,m)+1);
}
int main(){
int tc=read();
while(tc--) solve();
return 0;
}
ARC126 F Affine Sort
被两年前的 zhy 爆杀了 QwQ。
对于题目中的极限式,有如下事实:
Stolz 定理
当 \(g\) 是严格单调且趋近于无穷的数列时,有:
\[\lim_{n\to \infin} \frac{f_n}{g_n}=\lim_{n\to \infin} \frac{f_n-f_{n-1}}{g_n-g_{n-1}} \]
所以:
注意到 \(f(n)-f(n-1)\) 相当于钦定 \(c=n\) 的贡献,我们现在要算当 \(c\to \infin\) 时的答案。
当 \(c\) 趋近于无穷时,\(a,b\) 的取值越来越接近 \([0,c)\) 上的均匀分布。我们直接去考虑 \(a'=\frac{a}{c},b'=\frac{b}{c}\),答案只和这两个值有关,而且它们可以看成 \([0,1)\) 上的均匀随机变量。这就成了一道概率题了。
我们可以将一根 \([0,1)\) 的数轴首尾相接,那么原来的 \(aX_i+b\bmod c\) 可以看成第 \(i\) 个点以 \(X_i\) 的速度在这个环上走 \(a'\) 秒,然后再将整个环旋转 \(b'\) 单位长度。
最后的旋转相当于是说如果所有点移动结束后 \(1\sim n\) 以顺时针顺序排列,那么最终数组有序的概率就是点 \(1\) 与点 \(n\) 的距离。如果知道了合法的时间区间积分就做完了。
现在只要求所有点以顺时针顺序排列的概率。一种方法是求出所有点对相碰的时间暴力模拟整个过程,显然会 T。
我们可以考虑到求出相邻点有向距离之和,当且仅当恰好等于 \(1\) 时所有点以顺时针顺序排列。
容易发现相邻点有向距离的变性点只有 \(O(\sum X_i)\) 个。于是直接模拟,复杂度 \(O(\sum X_i\log \sum X_i)\)。
Code
#include <cstdio>
#include <algorithm>
using namespace std;
int read(){/*...*/}
const int N=1003,M=1000003,P=998244353;
typedef long long ll;
int n,cur,rk;
int a[N],b[N];
struct Event{
int t,d,x;
friend bool operator<(const Event u,const Event v){
return (ll)u.t*v.d<(ll)v.t*u.d;
}
}s[M];
int inv[M];
ll res;
void calc(int l,int r){
int pp=((ll)r*r-(ll)l*l)%P;
if(pp<0) pp+=P;
if(pp&1) pp+=P;
pp>>=1;
res=(res+(ll)pp*(a[1]-a[0])+(ll)b[1]*(r-l))%P;
}
int main(){
n=read();
for(int i=1;i<=n;++i) a[i]=read();
inv[1]=1;
for(int i=2;i<M;++i) inv[i]=(ll)inv[P%i]*(P-P/i)%P;
a[0]=a[n];
for(int i=1;i<=n;++i){
int dlt=a[i]-a[i-1];
if(dlt<0){dlt=-dlt;++b[i];++cur;}
for(int p=1;p<dlt;++p)
s[++rk]=(Event){p,dlt,i};
}
sort(s+1,s+rk+1);
int las=0;
for(int i=1;i<=rk;++i){
int cc=(ll)s[i].t*inv[s[i].d]%P;
if(cur==1) calc(las,cc);
las=cc;
int x=s[i].x;
if(a[x]>a[x-1]) --b[x],--cur;
else ++b[x],++cur;
}
if(cur==1) calc(las,1);
if(res<0) res+=P;
while(res%3) res+=P;
res/=3;
printf("%d\n",int(res));
return 0;
}
ARC125 F Tree Degree Subset Sum
由 Prufer 序我们知道只要 \(\sum deg_i=2n-2,deg_i\geq 1\) 啥树都能构造出来的。
于是我们将 \(deg_i\) 都减去 \(1\) 然后打表打出所有的 \((x,y)\),发现它竖着的 \(1\) 一定是一段区间???
于是对每种值记录一个选取个数的区间,将相同的 \(deg\) 压在一起多重背包。\(O(n\sqrt n)\) 。
Code
#include <cstdio>
#pragma GCC optimize(2,3,"Ofast")
using namespace std;
int read(){/*...*/}
const int N=400003,INF=0x3f3f3f3f;
int f[N],g[N],_f[N],_g[N];
int n,sm,d[N],cnt[N];
int qf[N],hdf,tlf;
int qg[N],hdg,tlg;
int main(){
n=read();
for(int i=1;i<n;++i) ++d[read()],++d[read()];
for(int i=1;i<=n;++i) ++cnt[--d[i]],f[i]=INF,g[i]=-INF;
f[0]=0;g[0]=cnt[0];
for(int x=1;x<n;++x){
if(!cnt[x]) continue;
for(int i=0;i<=sm;++i) _f[i]=f[i]-i/x,_g[i]=g[i]-i/x,f[i]=INF,g[i]=-INF;
int px=x*cnt[x];
int tsm=sm+px;
for(int t=0;t<x;++t){
hdf=hdg=1;tlf=tlg=0;
for(int i=t;i<=tsm;i+=x){
if(i<=sm){
while(hdf<=tlf&&_f[qf[tlf]]>=_f[i]) --tlf;
while(hdg<=tlg&&_g[qg[tlg]]<=_g[i]) --tlg;
qf[++tlf]=qg[++tlg]=i;
}
while(hdf<=tlf&&qf[hdf]<i-px) ++hdf;
while(hdg<=tlg&&qg[hdg]<i-px) ++hdg;
if(hdf<=tlf) f[i]=_f[qf[hdf]]+i/x;
if(hdg<=tlg) g[i]=_g[qg[hdg]]+i/x;
}
}
sm=tsm;
}
long long res=0;
for(int i=0;i<=sm;++i)
if(f[i]<=g[i]) res+=g[i]-f[i]+1;
printf("%lld\n",res);
return 0;
}
ARC124 F Chance Meeting
简单多项式?水铜题。
以下行列都是 0-indexed 的。容易发现两个人相遇只可能在同一行,设该行为 \(t\)。考虑容斥,钦定有 \(x\) 个相遇点,那么根据二项式反演或者其它手段可以知道容斥系数是 \((-1)^x x\)。先考虑不看那个 \(x\) 的系数,我们可以简单 DP 算出 \(-1\) 的贡献: \(f_u={t+H-t-1+u+u\choose t,H-t-1,u,u}-\sum_{v=1}^{u} f_{u-v} {2v\choose v}\),发现 \(u,t\) 是无关的可以分开算贡献,移下项发现是求逆的形式。
加上 \(x\) 的贡献也很好考虑,对于每一个钦定点 \(i\) 算贡献,包含它的钦定点列正好有 \(f_if_{W-i-1}\) 个,做完了。
为了这道题重构了一下求逆的板子,常数--。
Code
#include <cstdio>
#include <algorithm>
using namespace std;
int read(){/* read? */}
const int P=998244353,N=1<<20;
typedef long long ll;
int qp(int a,int b=P-2){
int res=1;
while(b){
if(b&1) res=(ll)res*a%P;
a=(ll)a*a%P;b>>=1;
}
return res;
}
int rev[N],cw[N+1];
int len,ilen;
void init(int _len){
len=1;int bt=-1;
while(len<_len) len<<=1,++bt;
cw[len]=cw[0]=1;
int w=qp(3,(P-1)>>(bt+1));
ilen=qp(len);
for(int i=1;i<len;++i){
rev[i]=(rev[i>>1]>>1)|((i&1)<<bt);
cw[i]=(ll)cw[i-1]*w%P;
}
}
void NTT(int *arr){
for(int i=0;i<len;++i)
if(i<rev[i]) swap(arr[i],arr[rev[i]]);
for(int i=1,t=1;i<len;i<<=1,++t)
for(int j=0;j<len;j+=(i<<1))
for(int k=j,tt=0;k<(j|i);++k,tt+=(len>>t)){
int x=arr[k],y=(ll)arr[k|i]*cw[tt]%P;
if((arr[k]=x+y)>=P) arr[k]-=P;
if((arr[k|i]=x-y)<0) arr[k|i]+=P;
}
}
void INTT(int *arr){
for(int i=0;i<len;++i)
if(i<rev[i]) swap(arr[i],arr[rev[i]]);
for(int i=1,t=1;i<len;i<<=1,++t)
for(int j=0;j<len;j+=(i<<1))
for(int k=j,tt=len;k<(j|i);++k,tt-=(len>>t)){
int x=arr[k],y=(ll)arr[k|i]*cw[tt]%P;
if((arr[k]=x+y)>=P) arr[k]-=P;
if((arr[k|i]=x-y)<0) arr[k|i]+=P;
}
for(int i=0;i<len;++i) arr[i]=(ll)arr[i]*ilen%P;
}
int tmp[N];
void INV(int *arr,int *res,int _len){
res[0]=qp(arr[0]);
for(int t=1;t<_len;t<<=1){
init(t<<2);
for(int i=0;i<(t<<1);++i) tmp[i]=arr[i];
for(int i=(t<<1);i<len;++i) tmp[i]=0;
NTT(res);NTT(tmp);
for(int i=0;i<len;++i){
res[i]=(2-(ll)tmp[i]*res[i])%P*res[i]%P;
if(res[i]<0) res[i]+=P;
}
INTT(res);
for(int i=(t<<1);i<len;++i) res[i]=0;
}
for(int i=_len;i<len;++i) res[i]=0;
}
int fac[N],fiv[N];
int F[N],G[N],H[N];
int n,m,lim,cur,res;
int main(){
n=read();m=read();
lim=n+n+m+m;
fac[0]=1;
for(int i=1;i<=lim;++i) fac[i]=(ll)fac[i-1]*i%P;
fiv[lim]=qp(fac[lim]);
for(int i=lim;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
for(int i=0;i<m;++i) H[i]=(ll)fiv[i]*fiv[i]%P*fac[i<<1]%P;
for(int i=0;i<m;++i) G[i]=(ll)fiv[i]*fiv[i]%P*fac[n-1+i+i]%P;
INV(H,F,m);init(m<<1);NTT(F);NTT(G);
for(int i=0;i<len;++i) F[i]=(ll)F[i]*G[i]%P;
INTT(F);
for(int i=0;i<n;++i){
int ttt=(ll)fiv[i]*fiv[n-1-i]%P;
cur=(cur+(ll)ttt*ttt)%P;
}
for(int i=0;i<m;++i) res=(res+(ll)F[m-1-i]*F[i])%P;
res=(ll)res*cur%P;
printf("%d\n",res);
return 0;
}
其实还有 \(O(n)\) 做法(orz zjk),我们发现我们需要求逆的东西形式比较特殊:\(G(x)=\sum_{i=0}^{\infin} {2i \choose i} x^i\)。回想起 Catalan 数的生成函数:\(C(x)=\frac{1-\sqrt{1-4x}}{2x}=\sum_{i=0}^{\infin} \frac{1}{i+1}{2i\choose i}x^i\),我们发现 \(G(x)=\frac{\mathrm{d}C(x)x}{\mathrm{d}x}\),对封闭形式求求导就可以轻松得到 \(G(x)=\frac{1}{\sqrt{1-4x}}\)。于是我们可以通过 \(O(n)\) 递推开根求出 \(G\) 的逆。
然而这道题是要求 \(H=\frac{F}{G}\),似乎依然需要一次卷积?但最后我们的答案只与 \([x^{m-1}]H^2\) 有关,也就是我们实际上只需要求 \(\frac{F^2}{G^2}=(1-4x)F^2\) 的单项,所以暴力求 \(F^2\) 的两个点值就可以了。