哈基题
AGC026E:字典序考虑 倒着 dp。
AGC036E:构造,贪心。
!AGC039F:\(\prod\) 的组合意义转化,矩阵 \((A,B)\) 计算的牛逼 dp。好题。
AGC067D:序列 + 区间 dp + 性质。
AGC067B:区间覆盖考虑区间 dp。
AGC045D:排列计数考虑 不断往置换环中插入数 + 容斥。
AGC062D:曼哈顿转切比雪夫 + 背包 + 图上游走 + bitset。
AGC008F:邻域计数 + 换根 dp。
P10042:曼哈顿距离 + 状压 dp。
ARC122F:建图 + dijkstra 求费用流(费用流原始对偶) + 前缀和优化建图。
AGC061F:LGV 引理 + 生成函数 + 二维拉格朗日插值。
AGC057E:\([0,1]\) 刻画 + 杨表性质 + dp。
P5115:SAM + parent 树 + 拆乘法贡献 + dp。
AGC037F:合并连续段的过程中 dp。
ARC138F:状压 + dp + 容斥。
ARC147F:环上转化 + \(\bmod 2\) 技巧 + 根号分治 + 生成函数 + 数位 dp。
ARC184E:前缀和转 差分 + 差分 的多项式表示 + 代表元 + \(\bmod 2\) 技巧 + 二维数点。
CF1984H:Geometry + 三角形外接圆。
ARC174F:博弈论 + dp + 维护差分。
CF1887E:矩阵转图论 + 二分 + 偶环。
CF2018F3:倒着做 + 容斥 + 倒着推 dp 系数(线性做法)。
AGC034D:MCMF + 曼哈顿距离最大的支配性(星野瑠美衣 弱弱弱化版)。
AGC014F:排列 + 思维 + premin 每次加入 \(\min\) 考虑额外贡献。
AGC028E:LIS 相关 + 字典序从前往后贪心 + 构造 + 抵消。
AGC053C:\(\min / \max\) 的计数方法,计算 \(\ge d / \le d\) 的个数 + 容斥 + 排列概率。
AGC030F:\(\min / \max\) 两两分组 + 扫值域做 dp。
AGC038F:置换环 + 排列 + 分讨 + 最小割。
AGC050F:\(\bmod 2\) 抵消 + 树形拓扑 + 思维。
AGC041F:广义笛卡尔树 + 容斥好题 + dp 容斥系数。
UOJ390:容斥 + 猎人杀技巧。
P5644:容斥 + 分治 NTT。
CF618F:前缀和 + 鸽巢原理 + 构造。
关于 \(\bmod 2\) 的小技巧:
AGC026E Synchronized Subsequence
S有一个长度为 \(2N\) 的仅由字符
a,b构成的字符串,且a的个数恰好等于b的个数,都出现了 \(N\) 次。你需要保留一些字符,剩下的字符删掉。对于一个 \(i\),你可以保留从左往右数的第 \(i\) 个
a和第 \(i\) 个b。注意,对于这两个字符,只能同时保留或同时删掉,不能只保留其中一个。
请你求出能得到的字典序最大的串。
\(1 \le N \le 3 \times 10^3\)。
直接做并不好做,怎么办?
由于求字典序最大,我们考虑 倒着 dp。
也就是设 \(dp_{i}\) 表示考虑 \(i\) 个即之后的每一对 ab,得到的字典序最大的串是多少。
那么转移首先有 \(dp_i=dp_{i+1}\),即不选当前的这一对 ab,要选则分两种情况
-
若
a在前面,那么我们在b出现之前一定不会选别的串了(选了相当于增加开头a的个数),这样一定不优。于是找到第一个起始位置在
b之后的位置 \(k\),\(dp_i \leftarrow ab+dp_k\)。 -
若 \(b\) 在前面,那么
a之前出现的一定都是b,我们一定要选,这样会更优。这样递归下去,直到一个位置的起始位置在当前最远的
a之后,假设为 \(k\),则 \(dp_{i} \leftarrow s[i,k-1] +dp_k\),其中 \(s[l,r]\) 表示把 \(l,r\) 这些对ab全部选上构成的串。
这样就做完了,直接做时间复杂度就是 \(\mathcal O(n^2)\) 的。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=6005;
int n,a[N],b[N],ca,cb,rev[N];
string dp[N];
char ch[N];
int main(){
/*2025.3.22 H_W_Y AT_agc026_e [AGC026E] Synchronized Subsequence 字典序 + dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=2*n;i++){
cin>>ch[i];
if(ch[i]=='a') a[++ca]=i,rev[i]=ca;
else b[++cb]=i,rev[i]=cb;
}
for(int i=n,j;i>=1;i--){
dp[i]=dp[i+1];
if(a[i]<b[i]){
j=i;
while(j<=n&&a[j]<=b[i]) ++j;
dp[i]=max(dp[i],"ab"+dp[j]);
}else{
j=i;int mx=a[i];
while(j<=n&&b[j]<=mx) mx=a[j],++j;
string tmp="";
for(int k=b[i];k<=mx;k++) if(rev[k]>=i) tmp.push_back(ch[k]);
dp[i]=max(dp[i],tmp+dp[j]);
}
}
cout<<dp[1]<<'\n';
return 0;
}
AGC036E ABC String
给你一个长度为 \(n\) 的仅包含
ABC三种字母的字符串 \(s\),现在要你输出一个满足下列要求的最长的 \(s\) 的子序列:
ABC三种字符的出现次数相同。- 子序列中相邻两个字符不能相同。
如果有多组解,输出任意一组即可。
\(|S| \le 10^6\)。
有点牛。
首先考虑我们第一步一定是把相邻的相同的都缩起来。
假设现在三个字母的个数是 \(c_a,c_b,c_c\),我们钦定 \(c_a \le c_b \le c_c\),那么答案的上界是 \(c_a\),我们不希望删除 A。
发现如果此时 \(c_a \lt c_b = c_c\),我们一定是可以在不删除 A 的情况下满足条件的。
构造就是每次删除 BC,容易发现这样一定是可以删完的,因为我们保证了 A 的数量最小。
那么现在就是对于 \(c_a \lt c_b \lt c_c\) 的情况,我们如何把 \(c_c\) 减变成 \(c_b\)。
同样考虑贪心,对于每个 A 中间如果形如 CBCBC,那么我们就把两边的 C 全部删掉。
如果都删完了还是 \(c_c \gt c_b\),此时就不得不删除 A 了,这种情况意味着存在非常多的 ACA,于是我们就直接删除一些 AC 即可。
这样就做完了,容易发现这样的长度保留下来是最长的。
时间复杂度线性。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,c[3],a[N],to[3],rev[3],pre[N],nxt[N];
string S;
void del(int x){pre[nxt[x]]=pre[x],nxt[pre[x]]=nxt[x];}
int main(){
/*2025.3.24 H_W_Y AT_agc036_e [AGC036E] ABC String good*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>S;
for(int i=0;i<(int)S.size();i++) if(!i||S[i]!=S[i-1]) a[++n]=S[i]-'A',++c[a[n]];
for(int i:{0,1,2}) to[i]=i;
sort(to,to+3,[&](int x,int y){return c[x]<c[y];}),sort(c,c+3);
for(int i:{0,1,2}) rev[to[i]]=i;
for(int i=1;i<=n;i++) a[i]=rev[a[i]],pre[i]=i-1,nxt[i]=i+1;
pre[n+1]=n,nxt[0]=1,a[0]=3,a[n+1]=4;
for(int i=nxt[0];i<=n&&c[2]>c[1];i=nxt[i])
if(a[i]==2&&a[pre[i]]!=a[nxt[i]]) del(i),--c[2];
for(int i=nxt[0];i<=n&&c[2]>c[1];i=nxt[i])
if(a[i]==2&&!a[pre[i]]) del(pre[i]),del(i),--c[0],--c[2];
if(c[1]!=c[2]) exit(0);
for(int i=nxt[0];i<=n&&c[1]>c[0];i=nxt[i])
if(pre[i]&&a[i]&&a[pre[i]]&&a[pre[pre[i]]]!=a[nxt[i]]) del(pre[i]),del(i),--c[1],--c[2];
for(int i=nxt[0];i<=n;i=nxt[i]) cout<<(char)(to[a[i]]+'A');
cout<<'\n';
return 0;
}
AGC039F Min Product Sum
有一个大小为 \(N \times M\) 的矩阵。矩阵中每个数的取值都是 \([1, K]\)。
对于一个矩阵,定义函数 \(f(x, y)\) 为:第 \(x\) 行和第 \(y\) 列的一共 \(N + M - 1\) 个数中的最小值。
对于一个矩阵,定义其权值为 \(\prod_{x=1}^N \prod_{y=1}^M f(x, y)\)。
你需要求出,对于所有 \(K^{NM}\) 种矩阵,每个矩阵的权值和对 \(D\) 取模的结果。
\(1 \leq N, M, K \leq 100\), \(10^8 \leq D \leq 10^9\),保证 \(D\) 为质数。
\(\prod\) 相当不好处理,我们考虑如何转化?
直接考虑它的组合意义,那么变成了计算两个矩形 \((A,B)\) 的数量满足 \(B\) 中的每一个元素都小于 \(A\) 中对应行 / 列最小值的方案数。
再转化一下,发现这个条件等价于计算两个矩形 \((A,B)\) 的数量满足
- \(B\) 中每一行的最大值 \(\le\) \(A\) 中对应行的最小值。
- \(B\) 中每一列的最大值 \(\le\) \(A\) 中对应列的最小值。
那么这个东西看起来就比较能 dp 了,我们容易想到从小到大扫 值域,那么对于 \(A,B\) 一个位置的限定我们希望独立开来。
也就是发现你在一个矩形中又确定行又确定列并不好处理。
所以考虑 dp 状态设计成 \(f_{k,i,j}\) 表示当前矩形填了 \(\le k\) 的数,确定了 \(B\) 中 \(i\) 行的最大值,\(A\) 中 \(j\) 列的最小值的方案数。
转移考虑 \(k+1\) 这些元素填在什么地方,分两步
- 确定一些 \(B\) 中的行的最大值为 \(k+1\)。
- \(A\) 中对应行已确定列的位置填 \(\ge k+1\)。
- \(B\) 中对应行未确定列的位置填 \(\le k+1\)(一定有一个 \(k+1\))。
- 确定一些 \(A\) 中的列的最小值为 \(k+1\)。
- \(A\) 中对应列已确定行的位置填 \(\ge k+1\)(一定有一个 \(k+1\))。
- \(B\) 中对应列未确定行的位置填 \(\le k+1\)。
容易发现这样的 dp 使得每个位置都能取到最严格的限制,于是分两步转移即可(在后面再加一维)。
这样就做完了,时间复杂度 \(\mathcal O(Knm(n+m))\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=105;
int n,m,K,H,C[N][N],f[N][N][N][2],g[N][N][N][2];
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int dec(int a,int b){return a<b?a-b+H:a-b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
int qpow(int a,int b=H-2){
int res=1;
while(b){if(b&1) res=mul(res,a);a=mul(a,a),b>>=1;}
return res;
}
void init(){
for(int i=0;i<N;i++){
C[i][0]=1;
for(int j=1;j<=i;j++) C[i][j]=adc(C[i-1][j],C[i-1][j-1]);
}
}
int main(){
/*2025.3.26 H_W_Y AT_agc039_f [AGC039F] Min Product Sum dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m>>K>>H,init();
for(int k=1;k<=K;k++)
for(int j=0;j<=m;j++){
int t=mul(qpow(K-k+1,j),dec(qpow(k,m-j),qpow(k-1,m-j)));
g[k][j][0][0]=1;
for(int i=1;i<=n;i++) g[k][j][i][0]=mul(g[k][j][i-1][0],t);
}
for(int k=1;k<=K;k++)
for(int i=0;i<=n;i++){
int t=mul(dec(qpow(K-k+1,i),qpow(K-k,i)),qpow(k,n-i));
g[k][i][0][1]=1;
for(int j=1;j<=m;j++) g[k][i][j][1]=mul(g[k][i][j-1][1],t);
}
f[0][0][0][1]=1;
for(int k=1;k<=K;k++){
for(int i=0;i<=n;i++)
for(int j=0;j<=m;j++) if(f[k-1][i][j][1])
for(int l=0;l<=n-i;l++)
add(f[k][i+l][j][0],mul(f[k-1][i][j][1],mul(C[n-i][l],g[k][j][l][0])));
for(int i=0;i<=n;i++)
for(int j=0;j<=m;j++) if(f[k][i][j][0])
for(int l=0;l<=m-j;l++)
add(f[k][i][j+l][1],mul(f[k][i][j][0],mul(C[m-j][l],g[k][i][l][1])));
}
cout<<f[K][n][m][1]<<'\n';
return 0;
}
AGC067D Unique Matching
定义 \(n\) 个区间是好的,当且仅当满足以下条件:
- 对于每个区间 \([l_i, r_i]\),有 \(1 \leq l_i \leq r_i \leq N\)。
- 存在唯一的 \(N\) 阶排列 \(x_1, x_2, \cdots, x_N\),使得对于所有 \(i\),\(x_i \in [l_i, r_i]\)。
给定整数 \(N\) 和素数 \(P\),求有多少组区间 \([l_1, r_1], [l_2, r_2], \cdots, [l_N, r_N]\) 是好的。答案需要对 \(P\) 取模。
\(2 \le N \le 5000\)。
首先我们可以认为最后确定出来的排列是 \((1,2,\cdots,n)\),于是最后算出答案再乘上一个 \(n!\) 即可。
考虑怎样的一组区间是合法的?
对于每一个 \(i\) 我们找到后面第一个 \(j\) 满足 \(L_j \le i\),如果 \(R_i \ge j\) 那么就不合法,因为这样可以交换 \(i,j\) 同样满足条件。
稍微归纳一下发现如果存在一个置换环满足条件,那么就一定可以通过交换两个元素得到?
于是得到了充要条件,即假设已经确定了 \(L\),设 \(a_i = \min \{j \mid j \gt i \land L_j \le i \}\),确定 \(r\) 的方法为 \(\prod (a_i-i)\),上述过程我们认为 \(L_{n+1}=0\)。
但是 \(a\) 序列并不是好表示的,所以我们有一个很好的思路:
假设直到当前 \(a\) 是多少,有多少个 \(L\) 序列满足条件呢?
那么稍微手玩一下发现我们可以把 \(a\) 和 \(L\) 一起做 dp 了,即采用区间 dp,设 \(f_{l,r}\) 表示考虑区间 \([l,r]\) 已知 \(L_{r+1} \lt l\),也就是 \(\forall k \in [l,r] ,a_k \le r+1\),的方案数。
那么对于 \(f\) 的转移我们考虑枚举 \(l\) 到了哪里,但是这里就需要另外一个 dp 了,也就是 \(g_{l,r}\) 表示考虑区间 \([l,r]\),已知 \(\forall k \in [l,r],a_k \le r+1\),但不知道 \(L_{r+1}\) 具体是多少的方案数。
连立起来得到转移
其中枚举 \(k\) 是相当于第一个 \(a_k=r+1\) 的位置,而 \((k-l+1)\) 是对 \(L_{r+1}\) 的决策,而 \((r+1-k)\) 是 \((a_i-i)\) 的贡献。
我们发现上述转移和 \([l,r]\) 无关,只与 区间长度 有关,所以转移变成
直接做时间复杂度 \(\mathcal O(n^2)\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=5005;
int n,H,f[N],g[N],ans;
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
int main(){
/*2025.3.26 H_W_Y AT_agc067_d [AGC067D] Unique Matching dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>H;
f[0]=g[0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
add(f[i],mul(i-j+1,mul(g[j-1],f[i-j]))),
add(g[i],mul(mul(i-j+1,j),mul(g[j-1],f[i-j])));
ans=f[n];
for(int i=1;i<=n;i++) ans=mul(ans,i);
cout<<ans;
return 0;
}
AGC067B Modifications
有一个长度为 \(N\) 的整数序列 \(a = (a_1, a_2, \cdots, a_N)\)。所有元素的初始值都是 \(0\)。
给定一个整数 \(C\) 和 \(M\) 个区间 \(([L_1, R_1], [L_2, R_2], \cdots, [L_M, R_M])\)。
你需要选择 \((1, 2, \cdots, M)\) 的排列 \(p\) 和长度为 \(M\) 的整数序列 \(w = (w_1, w_2, \cdots, w_M)\),满足 \(1 \leq w_i \leq C\)。
然后进行 \(M\) 次修改。第 \(i\) 次修改如下:
- 将 \(a_{L_{p_i}}, \cdots, a_{R_{p_i}}\) 改为 \(w_i\)。
可以保证 \(a\) 中的每个位置都被至少一个区间覆盖。
求所有修改后可以生成的不同的 \(a\) 的个数。输出答案模 \(998244353\)。
\(n \le 100\)。
区间覆盖操作非常不好定义和维护,所以我们考虑倒着做。
也就是给定最后的 \(a\) 序列,那么我们每一次的操作就是把一个数字相同的区间全部变成 ?,最后问是否可以把整个序列变得只有 ?。
而对于 区间覆盖操作,我们考虑区间 dp。
设 \(f_{l,r}\) 表示把 \([l,r]\) 中的区间操作到只有 ? 的方案数,这里只考虑在 \([l,r]\) 里面的操作区间。
考虑容斥,去计数不能全部变成 ? 的序列个数,那么最后剩下的东西一定是若干个数字和若干段 ?,并且不能再消了(也就是对于任何一个操作区间,它都存在至少两个不相同的数字)。
于是容易相当直接 dp,设 \(g_{l,r,p}\) 表示当前考虑 \([l,r]\) 区间,最后一个数字出现在 \(r\),上一个不同的数字出现在 \(p\) 的方案数。
转移就是枚举上一个数出现的位置 \(i\),分成两种情况,和上一个填一样的还是不一样的
注意这里的 \(p\) 需要满足一些条件,也就是对于一个右端点在 \([i,r)\) 的区间,它都必须要包含 \(p\),否则这个序列就可以继续消。
同理转化到 \(f\) 上面也就是枚举上一个数字在哪里''
同理这里需要满足对于任意一个右端点在 \([i,r]\) 中的区间,包含了 \(i\) 就必须包含 \(p\)。
这样就做完了,直接做时间复杂度 \(\mathcal O(n^4)\),但好像我 \(\mathcal O(n^5)\) 也过了,就是这样。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
const int N=105,H=998244353;
int n,m,C,f[N][N],g[N][N][N],pw[N];
vector<int> G[N];
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int dec(int a,int b){return a<b?a-b+H:a-b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
void del(int &a,int b){a=dec(a,b);}
int main(){
/*2025.3.25 H_W_Y AT_agc067_b [AGC067B] Modifications dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m>>C;
for(int i=1,l,r;i<=m;i++) cin>>l>>r,G[r].pb(l);
pw[0]=1;
for(int i=1;i<=n+1;i++) f[i][i-1]=g[i][i-1][i-1]=1,pw[i]=mul(pw[i-1],C);
for(int len=1;len<=n;len++){
for(int l=1,r=len;r<=n;++l,++r){
f[l][r]=pw[len];
for(int i=r-1,cur=0;i>=l-1;i--){
cur=0;
for(int j=i;j<r;j++) for(int l:G[j]) if(l<=i) cur=max(cur,l);
if(i==l-1&&cur<l) add(g[l][r][l-1],mul(C,f[l][r-1]));
for(int p=max(l-1,cur);p<i;p++)
add(g[l][r][p],mul(g[l][i][p],f[i+1][r-1])),
add(g[l][r][i],mul(C-1,mul(g[l][i][p],f[i+1][r-1])));
}
for(int i=r,cur=0;i>=l;i--){
cur=0;
for(int j=i;j<=r;j++) for(int l:G[j]) if(l<=i) cur=max(cur,l);
for(int p=max(l-1,cur);p<i;p++)
del(f[l][r],mul(g[l][i][p],f[i+1][r]));
}
}
}
cout<<f[1][n];
return 0;
}
AGC045D Lamps and Buttons
有 \(N\) 盏灯与 \(N\) 个开关,分别从 \(1\) 到 \(N\) 标号。起初,前面连续的 \(A\) 盏灯是被点亮的,而后面的灯是被熄灭的。
Snuke 和 Ringo 将要玩下面这个游戏:
- 首先,Ringo 生成一个 \(1\) 到 \(N\) 的排列 \((p_1, p_2, \ldots, p_N)\)。对于所有 \(N!\) 种可能的排列,每一种的概率都是等价的,而 Snuke 并不知道这个排列是什么。
- 然后,Snuke 可以做任意次如下操作:
- 选择一盏已经被点亮了的灯。(如果没有,那么这个操作将不可能被执行。)假设选取了第 \(i\) 盏灯,按下第 \(i\) 盏灯对应的按钮,那么第 \(p_i\) 盏灯的开关状态将会被改变。也就是说,如果第 \(p_i\) 灯是被点亮的,那么操作后它将被熄灭,反之亦然。
每一时刻,Snuke 都可以知道每一盏灯的开关情况。如果最终所有的灯都被点亮了,那么 Snuke 取得游戏胜利;而如果事实证明他无法取得胜利,他就会认输。如果 Snuke 采取最佳策略,那么他胜利的概率是多少呢?
假设 \(w\) 为胜利的概率,那么请输出 \(w \times N!\) 在模 \(10^9 + 7\) 意义下的值。
\(A \le 5000,N \le 10^5\)。
首先发现走到自环就寄了,那么每个点是自环的概率其实是一样的,所以我们的策略一定是
- 操作一个当前未操作过的编号最小的,如果是自环就失败,否则可以点亮它置换环里面的所有灯。
那么我们假设第一个自环的位置是 \(x\),如果我们能在 \(x\) 之前就把序列点亮那么就胜利了。
于是考虑枚举这个 \(x\),我们需要保证前面没有自环,并且 \(A+1 \sim N\) 所在的置换环都有一个数在 \(1 \sim x-1\) 中。
发现是否存在自环并不好处理,所以考虑容斥,去钦定至少有 \(y\) 个自环在前面,那么现在计数的问题就是。
求满足后 \(c\) 个数所在的置换环都存在至少一个前 \(a\) 个数,中间的 \(b\) 个数没有限制的排列数量,其中 \(a=x-1-y,b=A-x,c=N-A\)。
这个东西怎么做?
发现你可以在 置换环 中不断插入!!!
也就是考虑每新加入一个数,我们去考虑把它插在哪个置换环里面。
考虑先加入前 \(a\) 个再加入后 \(c\) 个,最后加入中间的 \(b\) 个,于是方案数容易发现是
这个东西就可以 \(\mathcal O(1)\) 计算了。
所以直接枚举 \(x,y\),总的时间复杂度是 \(\mathcal O(A^2)\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=5005,M=1e7+5,H=1e9+7;
int fac[M],ifac[M],inv[M],n,A,ans;
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
int qpow(int a,int b=H-2){
int res=1;
while(b){if(b&1) res=mul(res,a);a=mul(a,a),b>>=1;}
return res;
}
void init(){
fac[0]=inv[1]=1;
for(int i=2;i<=n;i++) inv[i]=mul(inv[H%i],H-H/i);
for(int i=1;i<=n;i++) fac[i]=mul(fac[i-1],i);
ifac[n]=qpow(fac[n]);
for(int i=n;i>=1;i--) ifac[i-1]=mul(ifac[i],i);
}
int calc(int a,int b,int c){return mul(fac[a+b+c],mul(a,inv[a+c]));}
int binom(int n,int m){
if(m<0||n<m) return 0;
return mul(fac[n],mul(ifac[m],ifac[n-m]));
}
int sgn(int x){return x&1?H-1:1;}
int main(){
/*2025.3.26 H_W_Y AT_agc045_d [AGC045D] Lamps and Buttons 容斥 + permutation*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>A,init();
for(int x=1;x<=A+1;x++)
for(int y=0;y<x;y++)
add(ans,mul(mul(sgn(y),binom(x-1,y)),calc(x-1-y,max(0,A-x),n-A)));
cout<<ans;
return 0;
}
AGC062D Walk Around Neighborhood
给定正整数 \(N\) 和 \(N\) 个正偶数 \(D_i\)。
在平面直角坐标系中,小麦初始在 \((0,0)\),每次他可以选取一个未被擦去的 \(D_i\),将其擦去,并从 \((x,y)\) 移动到 \((x',y')\) 使得 \(|x - x'| + |y - y'| = D_i\)。注意,所有坐标都是实数而非整数。例如,\(D_i = 2\),你可以从 \((0,0)\) 到 \((0.85486,1.14514)\)。
请问经过 \(N\) 次移动后,是否可以回到 \((0,0)\)。
如果可以的话,对于所有到达的点 \((x,y)\) 请求出 \(\max(|x| + |y|)\) 可能取到的最小值是多少。
\(1 \le N \le 2 \times 10^5,2 \le D_i \le 10^5\)。
首先,曼哈顿距离的随机游走并不好做,所以我们通过 \((x+y,x-y)\) 转化成 切比雪夫距离 上面的随机游走。
那么每次走到一个距离自己 \(\max\) 距离为 \(D_i\) 的正方形上面。
考虑如何判无解,容易发现将 \(d_i\) 排序之后如果 \(\sum_{i =1}^{n-1} d_i \lt d_n\) 一定无解,因为根本走不到 \(d_n\) 上面去。
反之,就一定有解,因为我们可以通过前面的 \(n-1\) 次操作,走到 \(d_n\) 的矩形上面,这样再用一步走回来就可以了。
于是这样的构造给出了一种答案为 \(d_n\) 的解,我们考虑缩小他,也就是去枚举答案 \(x\),看是否存在一种方案能构造得到 \(x\)。
容易发现 \(x \ge \frac {d_n} 2\),不然根本无法走 \(d_n\) 这一步。
而我们的策略一定是从原点出发,走一些步走到距离原点为 \(x\) 的正方形上面,再在正方形上面到处走,最后再走若干步回来。
容易发现,走过去和走回来是一样的,并且你能走到 \(x\) 正方形上面,那么就能走到任意一个点(因为切比雪夫距离的另一维是可以随便取的),同时因为 \(2x \gt d_n\),任意长度都可以在正方形上面到处走。
现在问题就变成找两个子集,使得它们都能走到 \(x\) 的正方形上面。
而这个东西如何判断,发现我们一定是把 \(d\) 分成两个部分 \(d_i \lt x\) 和 \(d_i \ge x\)。
如果只存在 \(d_i \lt x\) 的情况,容易发现我们的充要条件是 \(\sum d_i \ge x\),这样就一定可以走到。
而假设此时集合中存在一个 \(\ge x\) 的数 \(y\),考虑这个东西会把限制变得更松,也就是我们用 \(d \lt x\) 的走若干步之后,只要当前点距离为 \(y\) 的矩形和原点距离为 \(x\) 的矩形有交就可以了。
这意味着什么,发现实际上是 \(\sum d_i \ge y-x\),那么贪心的想,我们一定是取最小的 \(y\) 了。
于是如何判断它能走到一个距离为 \(x\) 的正方形上面?
我们考虑把 \(\lt x\) 的距离用背包维护,同时维护 \(\ge x\) 的最小的两个数 \(y_1,y_2\),那么合法当且仅当之前能凑出一个 \(\ge y_1-x\) 的数的情况下,剩下的还能凑出 \(y_2-x\),于是直接用 bitset 的 find_next 即可。
总时间复杂度 \(\mathcal O(\frac {n^2} {\omega})\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,d[N];
bitset<N> bs;
int main(){
/*2025.3.27 H_W_Y AT_agc062_d [AGC062D] Walk Around Neighborhood bitset + dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>d[i];
sort(d+1,d+n+1),bs[0]=1;
for(int i=d[n]/2,j=0,s=0;i<=d[n];i++){
while(j<n&&d[j+1]<i) ++j,bs|=(bs<<d[j]),s+=d[j];
int x=d[j+1]-i,y=j==n-1?i:d[j+2]-i,pos=bs._Find_next(x-1);
if(s-pos>=y) cout<<i<<'\n',exit(0);
}
cout<<"-1\n";
return 0;
}
AGC008F Black Radius
Snuke 君有一棵 \(n\) 个节点的全白的树,其中有一些节点他喜欢,有一些节点他不喜欢。他会选择一个他喜欢的节点 \(x\),然后选择一个距离 \(d\),然后将所有与 \(x\) 距离不超过 \(d\) 的节点都染成黑色,问最后有多少种可能的染色后状态。
两个状态不同当且仅当存在一个节点,它在两个状态中不同色。
\(2 \le N \le 2 \times 10^5\)。
等价于本质不同的邻域计数问题,接下来都考虑领域不是全树。
首先考虑没有关键点的情况,那么我们假设 \((x,d_1)\) 和 \((y,d_2)\) 的领域一样,那么当且仅当 \(x \to y\) 路径上面有 至多一个 子树被覆盖了一半,其它的都一定覆盖完了。
假设现在 \(d_1 \lt d_2\),那么我们把 \(y\) 往 \(x\) 移动一步,并且让 \(d_2--\),也一定满足条件(因为 \(x\) 可以覆盖到 \(y\) 的子树,那么 \(x\) 前面的一个点也可以覆盖到)。
而如果现在 \(d_1 = d_2\),那么未覆盖完的子树一定在路径中点,我们把 \(x,y\) 都往路径中点移动一下,并且让 \(d_1--,d_2--\),邻域也不变。
所以这样一来,我们发现对于一种领域 \((x,d)\) 是唯一的,其中 \(d\) 是最小的,否则都可以让 \(d\) 变得更小。
于是没有关键点的计数中,我们考虑统计 \((x,d)\) 的数量,满足不存在一个与之相邻的点 \(y\) 满足 \((y,d-1) = (x,d)\)。
由于 \(d\) 是邻域最小的,也就是说对于任何一个 \(y\) 都存在一个 \(y\) 子树外离 \(x\) 距离 \(\ge d-1\),显然我们取 \(y\) 是距离 \(x\) 深度最深的子树,那么计算 \(se_x\) 表示距离 \(x\) 第二深的子树,\(mx_x\) 表示最深的子树,我们需要满足 \(d \le \min(se_x+1,mx_x-1)\)。
考虑加入关键点,那么对于非关键的那些 \((x,d_1)\),我们考虑是否存在一个关键的 \((y,d_2)\) 使得邻域相等。
容易发现只要 \(y\) 在 \(x\) 已经覆盖完的子树中就可以了,所以根据这一点,我们可以找到一个下界,于是一个 \(x\) 的贡献区间也是容易计算的了。
直接换根 dp,时间复杂度线性。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define ll long long
const int N=2e5+5;
int n,sz[N],len[N],sel[N];
bool vis[N];
ll ans=0;
vector<int> G[N];
void dfs1(int u,int fa){
for(int v:G[u]) if(v!=fa){
dfs1(v,u),sz[u]+=sz[v];
int t=len[v]+1;
if(t>len[u]) sel[u]=len[u],len[u]=t;
else if(t>sel[u]) sel[u]=t;
}
}
void dfs2(int u,int fa,int d){
int fi=len[u],se=sel[u];
if(d>fi) se=fi,fi=d;
else if(d>se) se=d;
int r=min(se+1,fi-1),l=vis[u]?0:fi;
for(int v:G[u]) if(v!=fa&&sz[v]) l=min(l,len[v]+1);
if(sz[u]<sz[1]) l=min(l,d);
ans+=max(0,r-l+1);
fi=0,se=0;
for(int v:G[u]) if(v!=fa){
if(len[v]>=len[fi]) se=fi,fi=v;
else if(len[v]>=len[se]) se=v;
}
for(int v:G[u]) if(v!=fa){
if(v==fi) dfs2(v,u,max(d,len[se]+1)+1);
else dfs2(v,u,max(d,len[fi]+1)+1);
}
}
int main(){
/*2025.3.27 H_W_Y AT_agc008_f [AGC008F] Black Radius 邻域*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n,len[0]=-1;
for(int i=1,u,v;i<n;i++) cin>>u>>v,G[u].pb(v),G[v].pb(u);
for(int i=1;i<=n;i++){
char ch;cin>>ch;
sz[i]=vis[i]=ch=='1';
}
dfs1(1,0),dfs2(1,0,0);
cout<<ans+1;
return 0;
}
P10042 [CCPC 2023 北京市赛] 三染色
我有一个调色盘,总共 \(n\) 行 \(m\) 列,形成了 \(n\times m\) 个格子,每个格子里要放一朵花。可以放置的花有 \(3\) 种颜色可以选择,分别用 \(0,1,2\) 表示。
花朵注视着它周围的花,并想要变成其他花朵的样子。如果在一个时刻,一朵颜色为 \(c\) 的花的上、下、左、右之一,有至少一朵花的颜色为 \(c-1\),那么这朵花在下一个时刻会变成颜色 \(c-1\),否则它在下一个时刻的颜色仍然是 \(c\)。其中颜色 \(\bmod 3\) 考虑。
对于一个初始的在调色盘中放花的方案,如果经过有限个时刻之后,所有花都变成同一颜色,我们称这个放花的方案是美好的。
不难看出,对于一个美好的放花方案,每朵花都有一个最早的时刻,它在这个时刻之后一直不变色。我们称这个时刻为这朵花的稳定时刻。
我们从第 \(0\) 时刻开始计时,所以一朵花如果从未改变颜色,那么它的稳定时刻就是 \(0\)。现在我已经在调色盘的一些格子中放置了花朵,也有一些格子是空的。我想知道,有多少种给剩余的格子放花的方案,使得这个方案是美好的?以及,对于这些美好的方案,位于第 1 行第 1 列格子中花朵的稳定时刻的总和是多少?
你只需要回答我这两个结果对 \(998244353\) 取模的值。
\(2 \le n \le 5,2 \le m \le 50\)。
容易发现稳定状态下所有点颜色一定是一样的。
注意到 \(\bmod 3\) 的条件并不好处理,所以考虑如果没有 \(\bmod 3\),设这个矩阵为 \(b\),需要满足 \(b_{i,j} \equiv a_{i,j} \pmod 3\),那么我们一定是向全局的最小值靠拢,于是需要满足 \(|b_{i,j} - b_{x,y}| = 1\),其中 \((i,j)\) 与 \((x,y)\) 相邻。
于是只要确定了 \(b_{0,0}\),那么 \(a,b\) 就会形成双射。
我们希望给一个 \(a\) 找到一个 \(b\),找到 \(b\) 之后,\((0,0)\) 的稳定时刻其实是距离它最近的最小值(这里的距离为曼哈顿距离)。
而什么样的 \(a\) 才能找到一个 \(b\) 呢?
发现只要不存在类似于
的情况都是可以的,因为这种情况这四个元素就形成了环,就会一直减下去了。
所以对于第一问我们可以直接状压 dp,而对于第二问,我们则需要维护当前最小值和最小值所在位置,直接爆炸。
如何优化?
我们发现对于最小值的记录,我们只需要维护 \(b_{0,i} - b_{\min}\),并且对于最小值所在位置(也就是距离 \((0,0)\) 最小的位置),假设为 \(d\),如果到后面 \(i \gt d\) 了就一定不可能更新了。
所以这启发我们换一种东西维护,也就是去维护当前列需要 \(j \lt k\) 的时候才有可能比最小值小。
那么 dp 状态就是 \(f_{i,S,j,k}\) 和 \(g_{i,S,j,k}\) 分别表示前 \(i\) 列,第 \(i\) 列的状态是 \(S\),\(b_{0,i} - b_{\min} =j\) 且下一行若存在最小值,则需要行数 \(\lt k\) 才能成为最小距离的方案数和权值。
转移就是枚举 \(T\),当前列的状态,通过上一次的 \(j\) 和当前列的最小值算出这一列的最小值,看是否能被更新就可以了。
具体可以看代码。
时间复杂度 \(\mathcal O((n+m)nm3^{2n})\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=505,pw[6]={1,3,9,27,81,243},H=998244353,d[3][3]={{0,1,-1},{-1,0,1},{1,-1,0}};
int n,m,tot,f[2][250][N][6],g[2][250][N][6],a[N][N],tr[N][N],mn[N],ps[N],ansf,ansg,o=0;
bool ok[N][N];
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int dec(int a,int b){return a<b?a-b+H:a-b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
void del(int &a,int b){a=dec(a,b);}
int p(int S,int i){return S/pw[i]%3;}
int main(){
/*2025.3.27 H_W_Y P10042 [CCPC 2023 北京市赛] 三染色 dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m,tot=pw[n];
for(int i=0;i<n;i++) for(int j=0;j<m;j++) cin>>a[i][j];
for(int i=0;i<m;i++)
for(int S=0;S<tot;S++){
ok[i][S]=1;
for(int j=0;j<n;j++) ok[i][S]&=(a[j][i]==3||a[j][i]==p(S,j));
}
for(int S=0;S<tot;S++){
mn[S]=ps[S]=0;
for(int i=1,k=0;i<n;i++){
k+=d[p(S,i-1)][p(S,i)];
if(k<mn[S]) mn[S]=k,ps[S]=i;
}
for(int T=0;T<tot;T++){
tr[S][T]=1;
for(int i=1;i<n;i++){
int x=p(S,i-1),y=p(T,i-1),z=p(T,i),w=p(S,i);
tr[S][T]&=!(d[x][y]+d[y][z]+d[z][w]+d[w][x]);
}
}
}
o=0;
for(int S=0;S<tot;S++) if(ok[0][S]) f[0][S][-mn[S]][ps[S]]=1,g[0][S][-mn[S]][ps[S]]=ps[S];
for(int i=1;i<m;i++){
o^=1;
memset(f[o],0,sizeof(f[o])),memset(g[o],0,sizeof(g[o]));
for(int S=0;S<tot;S++) for(int j=0;j<=n+i;j++) for(int k=0;k<n;k++) for(int T=0;T<tot;T++){
if(!ok[i][T]||!tr[S][T]) continue;
int w=j+d[S%3][T%3]+mn[T],v=f[o^1][S][j][k];
if(!v) continue;
if(w>0||(w==0&&k-1<=ps[T])){
add(f[o][T][j+d[S%3][T%3]][max(k-1,0)],v);
add(g[o][T][j+d[S%3][T%3]][max(k-1,0)],g[o^1][S][j][k]);
}else{
add(f[o][T][-mn[T]][ps[T]],v);
add(g[o][T][-mn[T]][ps[T]],mul(v,ps[T]+i));
}
}
}
for(int S=0;S<tot;S++) for(int j=0;j<=n+m;j++) for(int k=0;k<n;k++)
add(ansf,f[o][S][j][k]),add(ansg,g[o][S][j][k]);
cout<<ansf<<' '<<ansg<<'\n';
return 0;
}
ARC122F Domination
给定 \(n\) 个红点和 \(m\) 个蓝点,要求移动蓝点,使每个红点的右上方都至少有 \(k\) 个蓝点。将位于 \((x_1,y_1)\) 的蓝点,移动到 \((x_2,y_2)\) 的代价为 \(|x_1-x_2|+|y_1-y_2|\)。求最小代价。
\(1 \le N,M \le 10^5,1 \le K \le \min(N,10)\)。
容易发现,我们把相互支配的红点删掉,那么最终留下来的一定是一个从左上到右下的阶梯。
首先考虑 \(K=1\) 的情况。
考虑 图论建模,对于每一个红点,我们 \(i+1 \to i\) 连边,\(S \to 1\) 连边,\(n \to T\) 连边。
如何判断一种方案合法?发现假设一个蓝点覆盖的区间为 \([l,r]\),那么我们将 \(l \to r+1\) 连边,如果存在 \(S \to T\) 的路径,则这个方案是合法的。
那么找到这样一个合法方案的最小值?
发现对于一个蓝点 \((X,Y)\),如果它覆盖了区间 \([l,r]\),那么 \(l \to i+n\) 连 \(\max(0,y_l-Y)\) 的边,\(i+n \to r+1\) 连 \(\max(0,x_r-X)\) 的边。
这张图 \(S \to T\) 的最短路就是答案。
注意到这张图的点数和边数是 \(\mathcal O(n^2)\) 的,所以考虑简单优化:前缀和优化建图。
即对于每个点新建立一些点,以权值 \(y_l-Y\) 的边为例,我们需要新建一列点使得 \(i \to i+1\) 边权为 \(y_{i}-y_{i+1}\),对于其它的三种情况同理。
这样点数就是 \(\mathcal O(n+m)\) 的了。
现在考虑拓展到 \(K \gt 1\) 的情况,我们同样考虑按照如下方式建图,只是直接改成网络流形式。
保证经过 \(i+n\) 的流量为 \(1\) 可以直接拆点完成。
于是我们跑 \(S \to T\) 的最小费用最大流就是答案。
这东西显然用 spfa 会超时,而由于 \(K\) 又比较小,初始的图中边权都是正的。
所以我们考虑 Dijkstra 费用流(原始对偶)。
类似于 Johnson,我们给每条边赋值一个势能,使得最终边权都非负。
那么假设最开始 \(h_u\) 表示原点到 \(u\) 的距离,然后一开始给每条边加一个势能 \(h_u - h_v\)。
但是操作一次之后有可能激活一些反向边,但是发现根据 \(w_{u,v} = -w_{v,u}\) 的性质,我们发现在 \(h_i += dis_i\) 即可。
这样每次都可以用 dijkstra 求最短路,那么费用流时间变成 \(\mathcal O(k n\log n)\) 级别的。代码。
这样就做完了,时间复杂度 \(\mathcal O(Kn \log n)\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=5e6+5;
const ll inf=1e16;
int n,m,K,idx,prel[N],sufl[N],prer[N],sufr[N];
struct Nod{
int x,y;
}a[N];
namespace MCMF{
int tot=1,head[N],S,T,cur[N];
ll dis[N],h[N];
bool vis[N];
struct Edge{
int v,nxt;
ll w,c;
}e[N<<2];
void add(int u,int v,int w=1e9,int c=0){
e[++tot]={v,head[u],w,c},head[u]=tot;
e[++tot]={u,head[v],0,-c},head[v]=tot;
}
bool dij(){
for(int i=0;i<=idx;i++) dis[i]=inf,cur[i]=head[i],vis[i]=0;
priority_queue<pair<ll,int> > Q;
Q.push({0,S}),dis[S]=0;
while(!Q.empty()){
int u=Q.top().second;Q.pop();
if(vis[u]) continue;
vis[u]=1;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v;ll w=e[i].w,c=e[i].c+h[u]-h[v];
if(w&&dis[v]>dis[u]+c) dis[v]=dis[u]+c,Q.push({-dis[v],v});
}
}
for(int i=0;i<=idx;i++) vis[i]=false;
return dis[T]<inf;
}
ll dfs(int u,ll flow){
if(u==T||!flow) return flow;
ll res=flow;vis[u]=1;
for(int i=cur[u];i&&res;i=e[i].nxt){
cur[u]=i;
int v=e[i].v;ll w=e[i].w,c=e[i].c+h[u]-h[v];
if(w&&!vis[v]&&dis[v]==dis[u]+c){
ll f=dfs(v,min(res,w));
res-=f,e[i].w-=f,e[i^1].w+=f;
}
}
return flow-res;
}
ll MinC=0;
ll dinic(){
ll res=0;
while(dij()){
ll flow=dfs(S,inf);
res+=flow,MinC+=(dis[T]-h[S]+h[T])*flow;
for(int i=0;i<=idx;i++) if(dis[i]<inf) h[i]+=dis[i];
}
return res;
}
}
using namespace MCMF;
int findl(int y){
int l=1,r=n,res=0;
while(l<=r){
int mid=((l+r)>>1);
if(a[mid].y>y) res=mid,l=mid+1;
else r=mid-1;
}
return res;
}
int findr(int x){
int l=1,r=n,res=n+1;
while(l<=r){
int mid=((l+r)>>1);
if(a[mid].x>x) res=mid,r=mid-1;
else l=mid+1;
}
return res;
}
signed main(){
freopen("in-01-006.txt","r",stdin);
/*2025.3.27 H_W_Y AT_arc122_f [ARC122F] Domination MCMF*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m>>K;
for(int i=1;i<=n;i++) cin>>a[i].x>>a[i].y;
sort(a+1,a+n+1,[&](Nod A,Nod B){return A.x==B.x?A.y>B.y:A.x<B.x;});
int _n=n;n=0;
for(int i=1;i<=_n;i++){
while(n&&a[i].y>=a[n].y) --n;
a[++n]=a[i];
}
a[0]={-1,(int)1e9+1},a[n+1]={(int)1e9+1,-1};
S=0;
for(int i=2;i<=n+1;i++) add(i,i-1,K,0);
add(S,1,K,0);
// cerr<<"a = ";
// for(int i=1;i<=n;i++) cerr<<" ( "<<a[i].x<<","<<a[i].y<<" ) ";
// cerr<<endl;
idx=n+m*2+1;
for(int i=0;i<=n+1;i++) prel[i]=++idx,sufl[i]=++idx;
for(int i=1;i<=n+1;i++) add(i,prel[i]),add(i,sufl[i]);
for(int i=0;i<=n;i++) add(prel[i],prel[i+1],1e9,a[i].y-a[i+1].y);
for(int i=1;i<=n+1;i++) add(sufl[i],sufl[i-1]);
for(int i=0;i<=n+1;i++) prer[i]=++idx,sufr[i]=++idx;
for(int i=0;i<=n;i++) add(prer[i],i+1),add(sufr[i],i+1);
for(int i=0;i<=n;i++) add(sufr[i],sufr[i+1],1e9,a[i+1].x-a[i].x);
for(int i=1;i<=n+1;i++) add(prer[i],prer[i-1]);
for(int i=1,x,y;i<=m;i++){
cin>>x>>y;
add(i+n+1,i+n+m+1,1,0);
int pos=findl(y);
add(prel[pos],i+n+1,1,a[pos].y-y);
add(sufl[pos+1],i+n+1,1,0);
pos=findr(x);
add(i+n+m+1,sufr[pos],1,a[pos].x-x);
add(i+n+m+1,prer[pos-1],1,0);
// for(int l=1;l<=n;l++) add(l,i+n+1,1,max(0,a[l].y-y));
// for(int r=1;r<=n;r++) add(i+n+m+1,r+1,1,max(0,a[r].x-x));
}
T=++idx,add(n+1,T,K,0);
assert(dinic()==K);
cout<<MinC<<'\n';
return 0;
}
AGC061F Perfect Strings
有正整数 \(n,m\),称一个非空
01串是好的当且仅当串中1的个数是 \(n\) 的倍数,0的个数是 \(m\) 的倍数。一个串是完美的,当且仅当其是好的,且其所有非空子串均不是好的。
对于给定的 \(n,m\),求完美的串的个数,答案对 \(998244353\) 取模。
\(1 \le n,m \le 40\)。
首先容易把问题转化到二维平面上面的游走,也就是说一个状态可以被表示成一个点 \((x,y)\) 表示当前前缀 \(cnt_0 \bmod n =x,cnt_1 \bmod m=y\)。
容易发现,问题等价于在 \(n \times m\) 的矩形上面走,从 \((0,0)\) 出发(左下角),最终回到 \((0,0)\) 的方案数,每次可以向右或者向上,不能经过重复点。
我们给这个网格的每一个入口(左边从上到下 \(1 \sim n\) 和下边从左到右 \(n+1\sim m\))和出口(一个入口标号对应一个出口,左侧与右侧对应)标号后,如果从左侧第 \(i\) 的一个入口进,那么就必须在右边的第 \(i\) 出口存在一条路出去。
并且假设左边入口数量为 \(i\),下面入口数量为 \(j\),我们需要保证 \(\gcd(i,j)=1\),否则就无法走过所有的路径(这和环上走问题类似)。
且对于第 \(n\) 和 \(n+1\) 个入口,也就是 \((0,0)\) 的左边入口 / 下边入口,只能选一个,这倒是小问题,我们可以假定从左边进入。
于是假设我们能枚举入口,那么问题就变成一个不交路径数量的问题,可以用 LGV 引理解决。
但是注意到由于我们出入口对应的排列的逆序对数为 \(i \times j\),所以算出行列式之后还需要乘上 \((-1)^{ij}\)。
显然去枚举入口是不现实的,也就是说我们考虑给网格图加一些边,从第 \(i\) 个入口连到第 \(i\) 个出口,这样就表示不选 \(i\)。
但是注意到我们最后还要计数左边入口个数和下边入口个数,所以对于每次选择的左边的入口,我们给它 \(\times x\),对于下面的入口,我们 \(\times y\)。
那么最终的出来的行列式就是一个关于 \(x,y\) 的二维多项式,我们关心的就是 \(\sum_{i,j} (-1)^{ij} [x^iy^j] F(x,y)\)。
关于这个行列式是否正确,我们可以用 LGV 的证明方法容易证得。
而加入那些 \(i \to i\) 的边之后,也并不会改变逆序对的奇偶性(写出排列就能发现)。
现在问题就只是如何求出这个带两个元的矩阵的行列式。
容易想到经典套路用 拉格朗日插值,我们对于 \((0\sim n,0\sim m)\) 的 \(nm\) 个点值分别求出 \(F(x,y)\) 的值,然后再插值回去就可以了。
那么复杂度 \(\mathcal O(nm(n+m)^3)\)。
还有一个小问题就是如何二维多项式的拉格朗日插值,假设我们知道 \(x_0,\cdots,x_n\) 和 \(y_0,\cdots y_m\) 这 \(n \times m\) 个网格的点值,\((x_i,y_j)\) 的点值为 \(f_{i,j}\),容易发现公式是
简单 dp 出每个 \(i\) 对 \(x\) 的系数,可以在 \(\mathcal O(n^2m^2)\) 的时间复杂度内暴力完成插值。
至于之前我们遗留下的 \(n,n+1\) 只能恰好选一个的问题,我们每次把其中一个的自己到自己的路径删掉,由于最终需要不交路径,所以另一边是一定要走的。
还有一种方法就是钦定走左边,然后把 \(n,m\) 交换再跑一次。
代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=105,H=998244353;
int n,m,f[N][N],g[N][N],res[N][N],val[N][N],C[N][N],a[N][N],ans;
int gcd(int a,int b){return !b?a:gcd(b,a%b);}
int sgn(int x){return x&1?(H-1):1;}
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int dec(int a,int b){return a<b?a-b+H:a-b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
void del(int &a,int b){a=dec(a,b);}
int qpow(int a,int b=H-2){
int res=1;
while(b){if(b&1) res=mul(res,a);a=mul(a,a),b>>=1;}
return res;
}
void init(int f[N][N],int n){
for(int i=0;i<=n;i++){
f[i][0]=1;
for(int j=0;j<=n;j++) if(j!=i){
for(int k=n;k>=1;k--) f[i][k]=mul(f[i][k],dec(0,j)),add(f[i][k],f[i][k-1]);
f[i][0]=mul(f[i][0],dec(0,j));
}
}
}
int Det(int len){
int res=1,rev=0;
for(int i=1;i<=len;i++){
if(!a[i][i])
for(int j=i+1;j<=len;j++) if(a[j][i]){swap(a[i],a[j]),rev^=1;break;}
if(!a[i][i]) return 0;
res=mul(res,a[i][i]);
int inv=qpow(a[i][i]);
for(int j=i+1;j<=len;j++) if(a[j][i]){
int cur=mul(inv,a[j][i]);
for(int k=i;k<=len;k++) del(a[j][k],mul(a[i][k],cur));
}
}
return rev?dec(0,res):res;
}
void build(int n,int m,int x,int y){
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++) a[i][j]=mul(x,C[i-j+m-1][m-1]);
for(int j=1;j<=m;j++) a[i][j+n]=mul(x,C[i-1+j-1][i-1]);
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++) a[i+n][j]=mul(y,C[n-j+m-i][n-j]);
for(int j=i;j<=m;j++) a[i+n][j+n]=mul(y,C[j-i+n-1][n-1]);
}
for(int i=1;i<=n+m;i++) add(a[i][i],1);
}
int calc(int x,int y){
build(n,m,x,y),del(a[n][n],1);
int res=Det(n+m);
build(n,m,x,y),del(a[n+1][n+1],1);
return adc(res,Det(n+m));
}
int main(){
/*2025.4.19 H_W_Y AT_agc061_f [AGC061F] Perfect Strings LGV 引理 + 二维 Lagrange 插值*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m;
init(f,n),init(g,m);
for(int i=0;i<N;i++){
C[i][0]=1;
for(int j=1;j<=i;j++) C[i][j]=adc(C[i-1][j],C[i-1][j-1]);
}
for(int i=0;i<=n;i++) for(int j=0;j<=m;j++) val[i][j]=calc(i,j);
for(int i=0;i<=n;i++) for(int j=0;j<=m;j++){
int fx=1,gy=1;
for(int x=0;x<=n;x++) if(x!=i) fx=mul(fx,dec(i,x));
for(int y=0;y<=m;y++) if(y!=j) gy=mul(gy,dec(j,y));
fx=qpow(fx),gy=qpow(gy);
for(int x=0;x<=n;x++) for(int y=0;y<=m;y++) add(res[x][y],mul(val[i][j],mul(mul(f[i][x],fx),mul(g[j][y],gy))));
}
for(int x=0;x<=n;x++) for(int y=0;y<=m;y++) if(gcd(x,y)==1) add(ans,mul(res[x][y],sgn(x*y)));
cout<<ans;
return 0;
}
AGC057E RowCol/ColRow Sort
给定一个 \(n \times m\),值域 \([0, 9]\) 的矩阵 \(B\),请你计数有多少个大小相同的矩阵 \(A\) 满足下列条件:
- 分别对 \(A\) 的 每一列 中元素从小到大排序,再分别对 \(A\) 的 每一行 中元素从小到大排序能够得到 \(B\)。
- 分别对 \(A\) 的 每一行 中元素从小到大排序,再分别对 \(A\) 的 每一列 中元素从小到大排序能够得到 \(B\)。
\(1 \leq n, m \leq 1500\),答案对 \(998244353\) 取模。
首先如何判断一个 \(A\) 合法?
发现带值域的东西并不好处理,所以我们考虑值域 \([0,1]\) 的情况。
那么容易发现条件等价于记录 \(r_i\) 表示第 \(i\) 行 \(0\) 的个数,\(c_j\) 表示第 \(j\) 列 \(0\) 的个数,那么\(A,B\) 两个矩形的 \(\{r_i \},\{ c_j\}\) 构成的可重集分别相等。
进一步的,我们发现 \(B\) 矩阵类似于杨表的性质,既然 \(A\) 可以得到它,那么当且仅当存在两个排列 \(p_i,q_i\) 使得 \(A_{i,j} = B_{p_i,q_j}\),也就是 \(A\) 是通过 \(B\) 进行若干次行列的交换得到的。
那么我们把这个东西拓展到值域 \([0,9]\) 的情况,相当于对于每一个 \(k \in [0,8]\),我们都需要存在两个排列 \(p_i,q_i\) 使得 \([A_{i,j} \le k] = [B_{p_i,q_j} \le k]\)。
接下来考虑如何计数。
假设我们知道了 \(k-1\) 对应的排列是 \(p',q'\),相当于我们知道了哪些位置是 \(\le k-1\) 的,那么当我们确定出 \(p,q\) 就知道哪些位置是 \(=k\) 的了。
容易发现,再计算 \((p,q)\) 时,我们是不关心 \((p',q')\) 的,并且对于任意一种 \((p',q')\),它都会被重复计算 \(\left(\prod c_i!\right) \left( \prod d_i!\right)\),其中 \(c_i\) 表示一行存在 \(i\) 个 \(0\) 的个数,\(d_i\) 表示一列存在 \(i\) 个 \(0\) 的个数,所以最后需要除掉。
那么我们现在就假设 \(p',q'\) 是原排列,于是每次对 \(p,q\) 的计数就是需要满足 \(B_{i,j} \le k-1 \to B_{p_i,q_j} \le k\)。
由于 \(B\) 看成 \(01\) 矩阵之后类似于 杨表,所以我们考虑利用行和列的单调性去解决这道题。
记 \(a_i\) 表示第 \(i\) 行 \(\le k-1\) 的个数,\(b_j\) 表示第 \(j\) 列 \(\le k\) 的个数,那么 \(a,b\) 都是单调不升的,上述条件转化成
进而,我们根据 \(a\) 的单调性,条件变成
于是一个非常自然的思路就是,设 \(x_i = \max_{j=1}^{a_i} q_j\),对于一个 \(x\),我们考虑有多少组 \((p,q)\) 满足条件。
考虑到 \(a\) 单调不升,\(b\) 单调不升,所以 \(x\) 单调不升,则 \(b_{x_i}\) 单调不降。
那么对 \(p\) 的贡献显然就是
再来考虑对 \(q\) 的贡献,我们考虑 \(x_i\) 和 \(x_{i-1}\) 的大小关系,那么每次中间少的就是 \((a_i,a_{i-1}]\) 这些元素。
若 \(x_i=x_{i-1}\),则说明 \((a_i,a_{i-1}]\) 中所有 \(q \lt x_{i-1}\),于是有贡献
其中 \(j-2\) 是因为,我们知道前面一定有一个 \(x_{i-1}\),然后其它的数都会 \(\lt x_{i-1}\),所以要把 \(=x_{i-1}\) 的位置刨掉。
若 \(x_i \lt x_{i-1}\),说明 \((a_i,a_{i-1}]\) 中有一个位置 \(q_j = x_{i-1}\),其它位置都 \(\lt x_{i-1}\),那么贡献就是
这三项的贡献都极其简单,那么我们就容易 dp 了,发现当前第 \(i\) 个部分的贡献都只关心与 \(x_i,x_{i-1}\),所以直接设 \(f_{i,j}\) 表示考虑前 \(i\) 个数,\(x_i = j\) 的答案。
转移去枚举上一个的 \(x_{i-1}\),容易用前缀和优化,所以总的时间复杂度为 \(\mathcal O(n^2 V)\)。
感觉这道题主要难在转化上面,后面的 dp 实际上是较为平凡的。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=1505,H=998244353;
int fac[N],ifac[N],a[N],b[N],n,m,mp[N][N],A[N][N],B[N][N],f[2][N],o,cnt[N],ans;
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
int qpow(int a,int b=H-2){
int res=1;
while(b){if(b&1) res=mul(res,a);a=mul(a,a),b>>=1;}
return res;
}
int calc(){
a[0]=m,b[0]=n,a[n+1]=0,f[o=0][m]=1;
for(int i=1,s;i<=n+1;i++){
o^=1,s=0;
for(int j=m;j>=0;j--){
f[o][j]=s;
if(j>=a[i-1]) add(f[o][j],mul(f[o^1][j],mul(fac[j-a[i]],ifac[j-a[i-1]])));
if(a[i]<a[i-1]&&j>=a[i-1]) add(s,mul(mul(f[o^1][j],a[i-1]-a[i]),mul(fac[j-a[i]-1],ifac[j-a[i-1]])));
if(i<=n) f[o][j]=mul(f[o][j],max(0,b[j]-i+1));
}
}
return f[o][0];
}
void init(){
fac[0]=1;
for(int i=1;i<N;i++) fac[i]=mul(fac[i-1],i);
ifac[N-1]=qpow(fac[N-1]);
for(int i=N-1;i>=1;i--) ifac[i-1]=mul(ifac[i],i);
}
int main(){
/*2025.4.22 H_W_Y [AGC057E] RowCol/ColRow Sort dp + Young*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m,init();
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){
cin>>mp[i][j];
for(int k=mp[i][j];k<=9;k++) ++A[i][k],++B[j][k];
}
ans=mul(fac[n],fac[m]);
for(int k=0;k<9;k++){
for(int i=1;i<=n;i++) a[i]=A[i][k];
for(int j=1;j<=m;j++) b[j]=B[j][k+1];
ans=mul(ans,calc());
}
for(int k=0;k<=9;k++){
for(int i=0;i<=m;i++) cnt[i]=0;
for(int i=1;i<=n;i++) cnt[A[i][k]]++;
for(int i=0;i<=m;i++) ans=mul(ans,ifac[cnt[i]]);
for(int i=0;i<=n;i++) cnt[i]=0;
for(int i=1;i<=m;i++) cnt[B[i][k]]++;
for(int i=0;i<=n;i++) ans=mul(ans,ifac[cnt[i]]);
}
cout<<ans;
return 0;
}
P5115 Check,Check,Check one two!
给定一个字符串
我们定义\(lcp(i,j)\)表示从字符串第\(i\)个位置开始的后缀和从第\(j\)个位置开始的后缀的最长公共前缀长度
我们定义\(lcs(i,j)\)表示在字符串第\(i\)个位置结束的前缀和在第\(j\)个位置结束的前缀的最长公共后缀长度
现在给定一个长度为 \(n\) 的字符串,希望您求出
\[\sum_{1\leq i < j \leq n}lcp(i,j)lcs(i,j)[lcp(i,j)\leq k1][lcs(i,j) \leq k2] \]模 \(2^{64}\) 的值(也就是unsigned long long自然溢出即可)
\(1 \leq n \leq 10^5,1\leq k1 , k2 \leq n\)。
max 的 SAM 牛逼题。
首先考虑没有后面的两个 \(\le k_1,\le k_2\) 的情况。
那么一个比较显然的思路就是我们在每一个 LCP 的位置统计 \(LCS(i,j)\) 的值。
具体来说,假设存在两个位置 \(i,j\),它们的 LCS 长度为 \(len\),我们加上 \(1 +2+\cdots + len\) 的贡献。
如果建出 parent 树,那么在每个节点维护 LCS 为 \(len\) 的点对个数即可 \(\mathcal O(1)\) 计算。
现在考虑有了 \([LCP(i,j)\le k_1][LCS(i,j)\le k_2]\) 的限制,我们同样考虑相同的方法,在 LCS 为 \(len\) 的节点计算贡献,但这里就只加到 \(k_2\) 了。
而对于 \(LCP(i,j) \le k_1\) 的贡献,同样我们需要在 \(len \ge k_1\) 的位置容斥掉,直接减去贡献就可以了。
这样就做完了,时间复杂度线性。代码。
AGC037F Counting of Subarrays
对于一个由正整数组成的序列 \(S\) 和两个正整数 \(k, l\),只要 \(S\) 满足下列两个条件之一,我们就称 \(S\) 属于级别 \((k, l)\)(一个序列可能同时属于多个级别):
- \(|S| = 1\) 且 \(S\) 中唯一的数字是 \(k\)。
- \(S\) 可以由 \(m\) 个属于级别 \((k - 1, l)\) 的序列 \(T_1, T_2, \ldots, T_m (m \geq l)\) 按顺序拼接而得到。
注意到当 \(k = 1\) 时第二个条件是无效的,所以,只有在满足第一个条件时,序列才可能属于级别 \((1, l)\)。
现在你有一个正整数序列 \(A_1, A_2, \ldots, A_N\) 和一个正整数 \(L\),求满足以下条件的连续子序列
\(A_i, A_{i+1}, \ldots, A_j (1 \leq i \leq j \leq N)\) 的数量:
- 存在一个正整数 \(K\),使得序列 \(A_i, A_{i+1}, \ldots, A_j (1 \leq i \leq j \leq N)\) 属于级别 \((K, L)\)。
\(1 \le n \le 2 \times 10^5\)。
首先考虑如何 check 一个序列合法。
一种比较显然的思路就是 倒着思考,每次合并连续 \(L\) 个相同的。
具体来说,我们找到最小值 \(x\) 所在的连续段,假设长为 \(len\),显然合并成 \(\left\lfloor \frac {len} L \right\rfloor\) 个 \(x+1\) 是最优的,因为这样下一次更容易成功。
根据这样的思路,我们就可以考虑在合并中计数了。
具体来说,我们给每一个位置赋两个权值 \(l_i,r_i\),分别表示 \(i\) 这个位置作为左端点 / 右端点的贡献,初始 \(l_i=r_i=1\)。
每次找到一个最小值 \(x\) 所在的连续段,计算只包含至少 \(L\) 个最小值的贡献(这是容易前缀和计算),然后把这个长度为 \(len\) 的连续段合并成 \(m=\left \lfloor\frac {len} L \right\rfloor\) 个 \(x+1\)。
那么新的 \(m\) 个点的权值就会进行一些改变。
具体的,\(\forall i \in [1,len],l'_{\left\lfloor \frac i L \right\rfloor} +=l_i,r'_{m-\left\lfloor\frac {len-i+1} L \right\rfloor+1} +=r_i\),这就意味着之后的合并当中,新节点对应被选为左 / 右端点的贡献。
然后注意需要容斥掉之后的合并中还是只选了这一段内部的贡献,直接减掉就可以了。
那么每次就这样合并,用链表维护还剩哪些位置,如果 \(len \lt L\) 就相当于把序列断开,而最小值所在位置可以用 set 维护。
这样就做完了,时间复杂度 \(\mathcal O(n \log n)\),不精细实现两个 \(\log\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=2e5+5;
int n,L,nxt[N],pre[N],a[N],id[N];
struct Nod{
int l,r;
}f[N],tmp[N],now[N];
set<array<int,2> > S;
ll ans=0;
ll calc(Nod *a,int n){
ll sum=0,res=0;
for(int i=1;i<=n;i++){
if(i>=L) sum+=a[i-L+1].l;
res+=sum*a[i].r;
}
return res;
}
int main(){
/*2025.4.23 H_W_Y AT_agc037_f [AGC037F] Counting of Subarrays dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>L;
for(int i=1;i<=n;i++) cin>>a[i],S.insert({a[i],i}),nxt[i]=i+1,pre[i]=i-1,f[i]={1,1};
while(!S.empty()){
int x=(*S.begin())[1],y=x,len=0,m=0;
for(;a[y]==a[x];y=nxt[y]) tmp[++len]=f[y],id[len]=y,S.erase({a[y],y});
m=len/L;
if(!m){nxt[pre[x]]=pre[y]=0;continue;}
ans+=calc(tmp,len);
for(int i=0;i<=m+1;i++) now[i]={0,0};
for(int i=1;i<=len;i++) now[m-(len-i+1)/L+1].l+=tmp[i].l,now[i/L].r+=tmp[i].r;
ans-=calc(now,m);
for(int i=1;i<=m;i++) f[id[i]]=now[i],S.insert({++a[id[i]],id[i]});
nxt[id[m]]=y,pre[y]=id[m];
}
cout<<(ans+n);
return 0;
}
ARC138F KD Tree
有一个长为 \(n\) 的点列 \(\{(i, p_i)\}\),每次可以选择 \(x/y\) 和某个坐标将点列分成左右/上下两边(保持两边相对顺序不变),然后分别递归下去,直到区间长度为 1。求可以得到多少本质不同的点列。
\(n \leq 30\)。
首先考虑简单 dp,设 \(f_{l,r,d,u}\) 表示只考虑 \(x\in[l,r],y \in [d,u]\) 矩形之内的点的方案数。
那么转移一个比较简单的思路就是去枚举从哪里分割。
也就是假设当前有 \(n\) 个点,那么我们会有 \(2(n-1)\) 中分割方式,按照 \(x\) 轴的分别为 \(x_1,\cdots,x_{n-1}\) 按照 \(y\) 轴的分别为 \(y_1,\cdots,y_{n-1}\)。
但是这里就注意到一个算重的问题了,有可能存在第一次分割不同的方式使得得到的排列是一样的。
所以我们考虑容斥,只在字典序最小的位置统计答案,也就是说,我们现在认为分割的字典序是 \(x_1,y_1,x_2,y_2,\cdots,x_{n-1},y_{n-1}\)。
那么设 \(f_{x_i}\) 表示第一次按照 \(x_i\) 分的左边的答案(去重之后),\(f_{y_i}\) 表示第一次按照 \(y_i\) 分的下边的答案(去重之后)。
对于当前的 \(f_{x_i}\),我们考虑去重,去枚举等价的划分方式 \(x_j,j \lt i\),容易发现需要去重 \(f_{x_j} f_{x_j+1,x_i,d,u}\)。
同理容斥等价划分方式 \(y_j,j \lt i\),那么推一下你发现平面中的四个区域一定有一个是空的,那么去重 \(f_{y_j}f_{l,x_i,y_j,u}\)。
对于当前的 \(f_{y_i}\),我们用同样的方式去重,只不过在 \(x_j\) 时存在 \(j = i\) 的情况,发现这种情况能成立当且仅当 \(x_i,y_i\) 划分出来的集合一样,于是对于空集我们把答案设成 \(1\),那么上面的计算是不会出问题的。
于是这样就做完了,时间复杂度 \(\mathcal O(n^6)\)。
注意到实际上 \(n\) 非常小,我们可以直接状压有哪些点出现了,然后转移是极其好写的。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
const int N=35,H=1e9+7;
int n,p[N],q[N];
map<int,int> mp;
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int dec(int a,int b){return a<b?a-b+H:a-b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
void del(int &a,int b){a=dec(a,b);}
int pc(int x){return __builtin_popcount(x);}
int dfs(int x){
if(pc(x)<=1) return 1;
if(mp.count(x)) return mp[x];
vector<int> a,b,g;
for(int i=0;i<n;i++) if(x>>i&1) a.pb(i),b.pb(p[i]);
sort(b.begin(),b.end());
for(int i=0,s0=0,s1=0;i<(int)a.size()-1;i++) g.pb(s0|=(1<<a[i])),g.pb(s1|=(1<<q[b[i]]));
int res=0;
vector<int> f((int)g.size());
for(int i=0;i<(int)g.size();i++){
f[i]=dfs(g[i]);
for(int j=0;j<i;j++) if((g[i]&g[j])==g[j]) del(f[i],mul(f[j],dfs(g[i]^g[j])));
add(res,mul(f[i],dfs(x^g[i])));
}
return mp[x]=res;
}
int main(){
/*2025.4.23 H_W_Y AT_arc138_f [ARC138F] KD Tree 状压 + 容斥*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=0;i<n;i++) cin>>p[i],--p[i],q[p[i]]=i;
cout<<dfs((1<<n)-1);
return 0;
}
ARC147F Again ABC String
有 \(T\) 组询问,每组询问给定 \(n, X, Y, Z\)。你需要计数满足如下条件的字符串 \(S\):
- 长度为 \(n\);
- 只包含 \(A, B\) 和 \(C\);
- 令 \(S_i\) 为 \(S\) 的前 \(i\) 个字符组成的串,\(A_i, B_i, C_i\) 分别为 \(S_i\) 中 \(A, B\) 和 \(C\) 的数量。对每个 \(1 \leq i \leq n\) 有
- \(A_i - B_i \leq X\);
- \(B_i - C_i \leq Y\);
- \(C_i - A_i \leq Z\)。
答案对 \(2\) 取模。
\(T \leq 10, 1 \leq n \leq 10^9, 0 \leq X, Y, Z \leq 10^9\)。
首先,我们考虑确定 \(S_i\) 的过程,那么每次相当于加一个字符,\(X,Y,Z\) 会有一加一减,一个不变。
这个东西并不好处理,但是我们发现上面的计算方式构成了一个环。
所以考虑往环上面转化,也就是我们构造一个长度为 \(m=X+Y+Z+3\) 的环,初始 \(0,X+1,X+Y+2\) 的位置分别有一颗棋子,那么每一次我们是把其中一颗棋子向正方向移动一步,要求全程不能出现两颗棋子位于相同位置的情况。
考虑利用答案 \(\bmod 2\) 的性质,容斥,总的方案数为 \(3^n\),那么我们需要减去存在两颗棋子位于相同位置的情况。
假设对于一种情况,最后一次两颗棋子相撞的时间为 \(i\),那么如果 \(i \neq n\),\(i+1\) 时刻走 \(A\) 和走 \(B\) 是本质相同(\(A,B\) 在 \(i\) 时刻相撞了)的,所以这里会计数出两种情况,而由于答案 \(\bmod 2\),所以这样不会对答案产生影响。
于是我们只关心与统计最后一次两颗棋子相撞的时间为 \(n\) 的答案,而如果最后三颗棋子都在一起,那么我们会多算两次,但是对 \(2\) 取模不会有影响,所以实际上我们不需要管它。
假设最后相撞的棋子是 \(A,B\),那么我们全过程只关心与 \(A,B\) 的相对位置,而每走一步可能造成的改变是 \(-1,0,1\),所以写出生成函数,我们要求的是
而考虑 \(\bmod 2\) 的经典 Trick
proof:直接把前者拆开。
\[\begin{aligned} & (x^{-1}+1+x)^{2^k} \\ = & \sum_{a+b+c=2^k} \binom {2^k} a \binom {2^k-a} b x^{-a} x^{2^k-a-b} \end{aligned} \]根据 Lucas 定理
\[\binom n m \bmod p = \binom {\lfloor n/p\rfloor} {\lfloor m/p\rfloor} \cdot \binom {n \bmod p} {m \bmod p} \]那么只有当 \(a=0 / 2^k\) 时 \(\binom {2^k} a=1\),其它情况都是 \(0\)。
对应的,我们可以算出 \(b\) 的取值,于是分类讨论出来就可以得到上式了。
于是原式就变成
其中 \(n = \sum_{i=1}^k 2^{p_i}\),也就是 \(p\) 是 \(n\) 的二进制拆分。
那么如果 \(m\) 比较小,我们每次可以暴力做循环卷积,时间复杂度 \(\mathcal O(m \log n)\)。
但是如果 \(m\) 比较大,上述式子并不是很成立。
所以我们换一种方法
注意到由于 \(r\le n\),所以实际上这里只会有 \(\mathcal O(\left \lfloor \frac nm \right \rfloor)\) 种可能的 \(r\),于是只要对于每一个 \(r\) 都有一种比较快的计算方式,那么还是可以接受的。
考虑去掉后面负指数的影响,也就是给全局乘上一个 \(x^n\),那么我们要求的就是
显然这个东西就可以数位 dp 了,我们把 \(r+n\) 也做二进制的展开,那么直接 dp 设 \(f_{i,0/1}\) 表示考虑 \(\ge i\) 位,是否需要在后面给第 \(i\) 位 \(1\) 的贡献,前面已经全部 \(=r+n\) 的 \(\ge i\) 位的方案数。
容易发现每一次考虑到第 \(i\) 位就考虑一下 \(n\) 是否存在 \(2^i\),然后看是否在第 \(i\) 位或第 \(i+1\) 位上面放 \(1\) 就可以了,转移是简单的。
这样就做完了,我们对 \(m\) 做根号分治,那么时间复杂度 \(\mathcal O(\sqrt n\log n)\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5+5,B=1000;
ll n,X,Y,Z,m;
bool f[N],g[N],dp[N][2];
bool SA(ll X,ll n,ll m){
for(int i=0;i<m;i++) f[i]=0;
f[0]=1;
for(int c=0;c<=30;c++) if(n>>c&1){
for(int i=0;i<m;i++) g[i]=f[i];
for(int i=0;i<m;i++) f[(i-(1ll<<c)%m+m)%m]^=g[i];
for(int i=0;i<m;i++) f[(i+(1ll<<c)%m+m)%m]^=g[i];
}
return f[X];
}
bool SB(ll X,ll n,ll m){
bool res=0;
for(ll r=(X+n)%m;r<=2*n;r+=m){
dp[31][0]=1;
for(int c=30;c>=0;c--){
dp[c][0]=dp[c][1]=0;
if(n>>c&1){
if(r>>c&1) dp[c][1]=dp[c+1][1]^dp[c+1][0],dp[c][0]=dp[c+1][0];
else dp[c][1]=dp[c+1][1],dp[c][0]=dp[c+1][0]^dp[c+1][1];
}else dp[c][r>>c&1]=dp[c+1][0];
}
res^=dp[0][0];
}
return res;
}
bool calc(ll X,ll n,ll m){return m<=B?SA(X,n,m):SB(X,n,m);}
void SOLVE(){
cin>>n>>X>>Y>>Z,m=(++X)+(++Y)+(++Z);
cout<<(1^calc(X,n,m)^calc(Y,n,m)^calc(Z,n,m))<<'\n';
}
int main(){
/*2025.4.24 H_W_Y AT_arc147_f [ARC147F] Again ABC String dp + 多项式 + 根号分治 + mod 2*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int _;cin>>_;
while(_--) SOLVE();
return 0;
}
ARC184E Accumulating Many Times
给定 \(N\) 个长度为 \(M\) 的整数序列,每个序列的元素是 \(0\) 或 \(1\)。第 \(i\) 个序列记为 \(A_i = (A_{i,1}, A_{i,2}, \dots, A_{i,M})\)。
我们定义一个函数 \(f(i, j)\),用于两个序列 \(A_i\) 和 \(A_j\) 之间的运算:
- \(f(i, j)\) 是最小的非负整数 \(x\),满足将 \(A_i\) 进行以下操作 \(x\) 次后,可以使得 \(A_i\) 和 \(A_j\) 相等。如果没有这样的 \(x\) 能够满足条件,则 \(f(i, j) = 0\)。
- 具体操作是:对于每个整数 \(k\ (1 \leq k \leq M)\),同时将 \(A_{i,k}\) 替换为 \(\left( \sum_{l=1}^{k} A_{i,l} \right) \bmod 2\)。
要求计算并输出 \(\sum_{i=1}^{N} \sum_{j=i}^{N} f(i, j)\),结果需要对 \(998244353\) 取模。
\(1 \leq N \times M \leq 10^6\)。
前缀和是不好处理的,我们考虑转化成 差分。
因为前缀和对于每一项会涉及到前面的所有项,而差分只会涉及到相邻两项,所以在一些生成函数等方面有着显著又是。
于是 \(f(i,j)\) 的定义变成从 \(A_j\) 通过每次差分变到 \(A_i\) 的步数,若不可行则为 \(0\)。
注意到对于一个序列 \(A\),\(\Delta A\) 是唯一的,所以我们将 \(A \to \Delta A\) 连边,容易发现最终构成了若干个环。
那么不在同一个环内肯定是互相没有贡献的,所以我们考虑一个环内的贡献。
我们给一个环找一个 代表元,为了方便就认为是环中 字典序 最小的串,对于每一个环中的串 \(i\),我们求出 \(d_i\) 表示 \(i\) 序列变到环上代表元的操作次数。
对于同一个环内的序列,贡献就是
其中 \(l\) 是环的长度。
那么我们现在先来考虑对于每个序列 \(A\),如何找到它所在环的代表元。
由于 差分 只会涉及到相邻两个元素,所以我们可以把 \(A\) 表示成多项式形式,那么知道
这里的多项式取模其实是按位取模,也就是 \(F(x) = \sum_{i=0}^n ([x^i](G(x)) \bmod 2)x^i\)。
于是我们对 \(A\) 操作 \(k\) 次就可以得到
这里又用到 \(\bmod 2\) 的经典 Trick 了,也就是上一道题用到的
证明和上一道题的 Trick 证明类似。
所以我们操作 \(k\) 次,相当于是拆成若干次操作 \(2^i\),每次
我们假设 \(A\) 的第一个 \(1\) 的位置在 \(t\)(全 \(0\) 特判),容易发现操作 \(2^i\) 第一个改变的位置是 \(t+2^i\),而第 \(t\) 位会永远为 \(1\)。
所以我们考虑贪心,从小到大枚举 \(2^i\),去看操作 \(2^i\) 次是否会让序列变得字典序更小,每次 \(\mathcal O(m)\) 暴力修改。
这样我们就可以在 \(\mathcal O(m \log m)\) 的时间复杂度之内找到当前序列对应的代表元,而当前的 \(d = \sum 2^i\),其中 \(i\) 是那些操作过的长度。
于是知道每个序列属于哪个环之后,我们还需要知道这个环的环长 \(l\)。
发现这是简单的,相当于的 \(\max d+1\),而 \(\max d\) 肯定是每一个二进制位都操作一次,所以
然后对于每一个环,需要计数的问题就是一个二维数点,直接用树状数组维护即可。
总的时间复杂度 \(\mathcal O(nm \log m)\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define pb push_back
const int N=1e6+5,H=998244353;
const ull bas=233;
int n,m,d[N],len[N],L[N],ans;
ull hs[N],b[N];
bool a[N];
vector<int> V[N];
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int dec(int a,int b){return a<b?a-b+H:a-b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
void del(int &a,int b){a=dec(a,b);}
namespace BIT{
int tr[N],n;
int lowbit(int i){return i&(-i);}
void init(int _n){n=_n;for(int i=0;i<=n;i++) tr[i]=0;}
void upd(int x){for(int i=x;i<=n;i+=lowbit(i)) ++tr[i];}
int qry(int x){
int res=0;
for(int i=x;i>=1;i-=lowbit(i)) res+=tr[i];
return res;
}
}
int main(){
/*2025.4.24 H_W_Y AT_arc184_e [ARC184E] Accumulating Many Times 差分 + 图论 + mod 2*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1,t=0;i<=n;i++){
t=m+1;
for(int j=1;j<=m;j++) cin>>a[j],t=min(t,a[j]?j:(m+1));
if(t>=m){len[i]=1;continue;}
for(int k=0;k<=__lg(m-t);k++) if(a[t+(1<<k)]){
d[i]|=(1<<k);
for(int j=m;j>t;j--) a[j]^=a[j-(1<<k)];
}
for(int j=1;j<=m;j++) hs[i]=hs[i]*bas+a[j];
len[i]=1<<(__lg(m-t)+1),b[i]=hs[i];
}
sort(b+1,b+n+1);
for(int i=1;i<=n;i++){
int pos=lower_bound(b+1,b+n+1,hs[i])-b;
V[pos].pb(d[i]),L[pos]=len[i];
}
for(int c=1;c<=n;c++) if(V[c].size()&&L[c]>1){
BIT::init(L[c]);
for(int i=0,sum=0;i<(int)V[c].size();i++){
add(ans,adc(dec(mul(i,V[c][i]),sum),mul(i-BIT::qry(V[c][i]+1),L[c])));
add(sum,V[c][i]),BIT::upd(V[c][i]+1);
}
}
cout<<ans;
return 0;
}
CF1984H Tower Capturing
给定二维平面上的 \(n\) 个点 \((x_i, y_i)\),保证没有任意两点重合、没有任意三点共线、没有任意四点共圆。初始时,你拥有点 \(1\) 和点 \(2\),剩下 \(n - 2\) 个点是未拥有的。
每次操作可以选择三个不同的点 \(P, Q, R\),需要满足:
- 你拥有点 \(P, Q\);
- 未拥有点 \(R\);
- 所有的 \(n\) 个点都在过 \(P, Q, R\) 得到的圆内。
然后你拥有 \(\triangle PQR\) 内的还未拥有的点(包括点 \(R\))。
问有多少种不同的操作序列能够让你拥有所有点。两个操作序列不同当且仅当:
- 它们的长度不同;或
- 某一步骤选择的无序三元组 \((P, Q, R)\) 不同。
保证 \(n \leq 100\), \(\sum n \leq 1000\), \(0 \leq |x_i|, |y_i| \leq 10^4\)。
我的 geo 水平要为负数了/ll
考虑对于两个点 \(A,B\),至多只有两个点 \(C\) 满足 \((A,B,C)\) 的外接圆会包含全集。
首先容易发现 \(A,B,C\) 一定都是凸包上面的点,并且对于 \(AB\) 划分出的平面两边,合法的 \(C\) 都需要满足 \(\angle ACB\) 最小(这样才能保证于它同侧的点都在圆内),于是 \(C\) 只有可能一边一个。
并且接下来,假设我们找到了 \(\triangle ABC\),那么进而继续找合法三角形时,如果包含 \(AC\),则 \(B'\) 一定不会在 \(B\) 一侧,同理,包含 \(BC\) 则 \(A'\) 一定不会在 \(A\) 一侧,这和上面的证明一样。
所以实际上合法的三角形只有 \(\mathcal O(n)\) 个,并且任意两个的面积交为 \(0\),构成了一个可以 dfs 的树结构。
那么我们直接在这个结构上面 dfs,每次出来实际上是多乘一个组合数就可以了。
每次暴力枚举一个点,判断 \(\triangle ABC\) 的外接圆是否包含左右点就可以了,时间复杂度 \(\mathcal O(n^3)\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define db long double
const int N=105,H=998244353;
const db eps=1e-5;
int n,C[N][N];
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
struct Nod{
int x,y;
friend Nod operator +(Nod A,Nod B){return {A.x+B.x,mul(C[A.x+B.x][A.x],mul(A.y,B.y))};};
}a[N];
void init(){
for(int i=0;i<N;i++){
C[i][0]=1;
for(int j=1;j<=i;j++) C[i][j]=adc(C[i-1][j-1],C[i-1][j]);
}
}
void calc(int i,int j,db &A,db &B,db &C){
Nod x=a[i],y=a[j];
A=(x.x-y.x),B=(x.y-y.y),C=-A*(x.x+y.x)/2.0-B*(x.y+y.y)/2.0;
}
db sq(db x){return x*x;}
bool chk(int A,db x,db y){
db d=sq(a[A].x-x)+sq(a[A].y-y);
for(int i=1;i<=n;i++) if(sq(a[i].x-x)+sq(a[i].y-y)>d+eps) return false;
return true;
}
Nod dfs(int A,int B,int fa){
Nod res={0,1};
for(int C=1;C<=n;C++) if(C!=A&&C!=B&&C!=fa){
db A1,B1,C1,A2,B2,C2,g;
calc(A,C,A1,B1,C1),calc(B,C,A2,B2,C2),g=A1*B2-B1*A2;
if(chk(A,(B1*C2-C1*B2)/g,(C1*A2-A1*C2)/g)){
Nod o=dfs(B,C,A)+dfs(A,C,B);
++o.x,res=res+o;
}
}
return res;
}
void SOLVE(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i].x>>a[i].y;
Nod res=dfs(1,2,0);
cout<<(!res.x?0:res.y)<<'\n';
}
int main(){
/*2025.4.24 H_W_Y CF1984H Tower Capturing GEO*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int _;cin>>_,init();
while(_--) SOLVE();
return 0;
}
ARC174F Final Stage
Alice 和 Bob 玩游戏。
有 \(N\) 个回合。给出 \(L_1...N\), \(R_1...n\),编号为奇数的回合是 Alice 玩,偶数回合是 Bob 玩。
有一堆石子,现在 \(i \leftarrow 1...n\) 表示游戏回合数。每次当前玩家取出一些石子,个数为 \([L_i, R_i]\),不能取的玩家输,然后对手赢。有 \(Q\) 次询问,每次给出 \(C\) 表示石子个数,求谁能赢,或者都不会赢。
\[1 \leq N \leq 3 \times 10^5, \, 1 \leq L_i \leq R_i \leq 10^9, \, 1 \leq Q \leq 3 \times 10^5$$。 \]
还是要从暴力的 dp 做起。
设 \(f_{i,j}\) 表示考虑 \(i \sim n\),初始棋子数量为 \(j\) 的答案。
那么容易发现一定是一段一输一赢交题,并且对于 \(i+1\) 的一个输的段 \([l,r]\),那么在 \(i\) 中 \([l+L_i,r+R_i]\) 都会是赢。
这启发我们去维护差分,也就是维护输赢交替的位置,那么每一次转移其实是把奇数位置都 \(+L_i\),偶数位置都 \(+R_i\),然后如果存在相交,那么两个都删掉了。
最后再在开头加上一个 \(0\) 位置的分界点,同时交换一些奇偶性。
于是这个差分数组就可以链表去维护了,每次我们暴力把交叉的地方删掉就可以了。
查询的时候可以直接在上面二分,注意维护最后一段是 Draw 的时候有一点小细节。
这样就做完了,时间复杂度 \(\mathcal O(n \log n)\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define fi first
#define se second
const int N=1e6+5;
int n,m,tot=0,pr[N],sf[N],hd=0,Q;
ll tl,tr,L[N],R[N],p[N],ans[N];
set<pair<ll,int> > ql,qr;
int main(){
/*2025.4.25 H_W_Y AT_arc174_f [ARC174F] Final Stage dp + 博弈 + 差分*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>L[i]>>R[i];
tot=2,sf[1]=2,pr[2]=1,p[2]=L[n],hd=1;
ql.insert({L[n],1});
for(int i=n-1;i>=1;i--){
tl+=L[i],tr+=R[i];
while(qr.size()&&(*qr.begin()).fi<=tr-tl){
int x=(*qr.begin()).se,y=sf[x];qr.erase(qr.begin());
if(!sf[y]){sf[x]=0;continue;}
if(!pr[x]) hd=sf[y];
pr[sf[y]]=pr[x],sf[pr[x]]=sf[y];
if(sf[y]) ql.erase({p[sf[y]]-p[y],y});
if(pr[x]) ql.erase({p[x]-p[pr[x]],pr[x]});
if(pr[x]&&sf[y]) ql.insert({p[sf[y]]-p[pr[x]],pr[x]});
}
swap(tl,tr),swap(ql,qr);
sf[++tot]=hd,pr[hd]=tot,p[tot]=-tl,hd=tot;
ql.insert({p[sf[tot]]-p[tot],tot});
}
for(int x=hd;x;x=sf[x]) ++m,ans[m]=p[x]+(m&1?tl:tr);
cin>>Q;
while(Q--){
ll x,pos;cin>>x;
pos=upper_bound(ans+1,ans+m+1,x)-ans-1;
cout<<(pos==m?"Draw":(pos&1?"Bob":"Alice"))<<'\n';
}
return 0;
}
CF1887E Good Colorings
Alice 和你玩游戏。有一个 \(n \times n\) 的网格,初始时没有颜色。Alice 在游戏开始前依次给其中 \(2n\) 个格子分别涂上了第 \(1 \sim 2n\) 种颜色,并告诉你每个颜色的位置。
接下来的每次操作,你可以选择一个未涂色的格子,由 Alice 在 \(2n\) 种颜色中选择一个涂在该格子上,并告诉你该颜色。
如果在某次操作后方格图上存在四个不同颜色的点,且它们的位置形成一个平行于边线的矩形,则输出它们以获得胜利。
你至多进行 10 次操作,请构造一个获胜方案。交互库自适应,也就是说 Alice 的决策与你的选择有关。
\[ T \leq 200, \, n \leq 1000$$。 \]
首先对于矩阵涉及到一行一列的问题,我们考虑 经典套路。
分成一个 \(2n\) 个点的二分图,左部点为行右部点为列,那么每一个格子就是行 \(\to\) 列的一条边。
那么由于连出了 \(2n\) 条边,所以一定会存在一个偶环。
我们的目的是构造一个互不相同的长度为 \(4\) 的环,那么每次在环上二分,在中间连一条边,那么左右两个小环一定存在一个是全都不相同的。
于是我们这样不断二分下去就可以了,只需要 \(\log\) 次。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
const int N=2e3+5;
int n,m,rt,f[N][N];
vector<int> G[N],V,q;
bool vis[N],in[N];
bool dfs(int u,int fa){
vis[u]=in[u]=1,q.pb(u);
for(int v:G[u]) if(v!=fa){
if(in[v]){
while(q.size()&&q.back()!=v) V.pb(q.back()),q.pop_back();
V.pb(v);
return true;
}
if(dfs(v,u)) return true;
}
q.pop_back();
return false;
}
void init(){
for(int i=1;i<=2*n;i++){
for(int j:G[i]) f[i][j]=0;
G[i].clear(),vis[i]=in[i]=0;
}
V.clear(),q.clear();
}
void add(int u,int v,int w){
G[u].pb(v),G[v].pb(u);
f[u][v]=f[v][u]=w;
}
void SOLVE(){
cin>>n;
for(int i=1,x,y;i<=2*n;i++) cin>>x>>y,add(x,y+n,i);
for(int i=1;i<=n;i++) if(!vis[i]&&dfs(i,0)) break;
while((int)V.size()>4){
int len=(int)V.size()-1,mid=len/4,x=V[mid],y=V[len-mid],col=0;
if(x>n) swap(x,y);
cout<<"? "<<x<<' '<<(y-n)<<'\n',cout.flush();
cin>>col,add(x,y,col);
bool fl=false;
for(int i=mid;i<len-mid;i++) fl|=(f[V[i]][V[i+1]]==col);
vector<int> now;
if(fl){
for(int i=0;i<=mid;i++) now.pb(V[i]);
for(int i=len-mid;i<=len;i++) now.pb(V[i]);
}else for(int i=mid;i<=len-mid;i++) now.pb(V[i]);
swap(V,now);
}
sort(V.begin(),V.end());
cout<<"! "<<V[0]<<' '<<V[1]<<' '<<(V[2]-n)<<' '<<(V[3]-n)<<'\n',cout.flush();
string S;cin>>S;
if(S[0]=='E') exit(0);
init();
}
int main(){
/*2025.4.25 H_W_Y CF1887E Good Colorings 矩阵 + 图论 + 二分*/
int _;cin>>_;
while(_--) SOLVE();
return 0;
}
CF2018F3 Speedbreaker Counting (Hard Version)
This is the statement of Problem D1B:
There are \(n\) cities in a row, numbered \(1, 2, \ldots, n\) left to right.
- At time \(1\), you conquer exactly one city, called the starting city.
- At time \(2, 3, \ldots, n\), you can choose a city adjacent to the ones conquered so far and conquer it.
You win if, for each \(i\), you conquer city \(i\) at a time no later than \(a_i\). A winning strategy may or may not exist, also depending on the starting city. How many starting cities allow you to win?
For each \(0 \leq k \leq n\), count the number of arrays of positive integers \(a_1, a_2, \ldots, a_n\) such that
- \(1 \leq a_i \leq n\) for each \(1 \leq i \leq n\);
- the answer to Problem D1B is \(k\).
The answer can be very large, so you have to calculate it modulo a given prime \(p\).
\(1 \le n \le 3000\)。
很牛的线性做法。
首先考虑 1B 的做法,我们很容易想到去 倒着做,首先来判定是否存在 \(ans \gt 0\)。
假设当前剩余的区间为 \([l,r]\),如果 \(a_l \ge r-l+1\) 我们就让 \(l+1\),同理如果 \(a_r \ge r-l+1\) 我们就让 \(r-1\),如果走不动了就说明不合法。
接着是如何求答案,容易发现我们可以在上述判定过程中在 \(l=i/r=i\) 的时候去判断 \(i\) 位置是否合法,因为最终 \([l,r]\) 会变成空集,所以每一个 \(i\) 都会走到。
而判定的时候我们发现条件是 \(\forall j \ge i,\max(j-a_j) \le i-1\) 和 \(\forall j\le i,\min(a_j+j) \ge i+1\),于是找到第一个满足条件一的 \(L\) 和最后一个满足条件二的 \(R\),容易发现 \([L,R]\) 区间内都是合法答案。
答案是区间的性质就非常厉害了。
所以我们考虑 dp,设 \(g_i\) 表示 \(ans =i\) 的方案数,也就是答案;考虑 容斥,设 \(f_i\) 表示钦定了一个长度为 \(i\) 的满足条件的区间的序列方案数,那么我们知道
所以只要我们能算出 \(f\),那么 \(g\) 的计算可以倒着线性递推出来,所以我们现在只关心与 \(f\) 的求法了。
那么问题就变得简单很多,我们把过程分两步:
- 计算长度为 \(i\),值域为 \(n\) 的全部合法的序列数量 \(h_i\)。
- 从长度为 \(i\) 的合法区间,递推到长度为 \(n\) 的合法区间。
对于前者,我们知道
容易把这个东西表示成阶乘形式,每次可以做到 \(\mathcal O(1)\) 计算。
而对于后者,我们先令 \(dp_i=h_i\),接下来的转移就是枚举下一次删掉的左边还是右边,容斥掉两边都可以同时删的代价,转移就是
那么,递推之后 \(f_i=dp_n\)。
上述过程已经可以做到 \(\mathcal O(n^2)\),可以通过。
但显然我们可以优化,考虑 转置,然后倒着做 dp,dp 出每一个状态对 \(dp_n\) 的转移系数,也就是最开始我们令 \(dp_{n}=1\)。
那么每次 \(f_i = dp_i \times h_i\),于是上述时间复杂度变成线性。
所以总的时间复杂度线性。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,fac[N],ifac[N],f[N],g[N],h[N],dp[N],H;
int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int dec(int a,int b){return a<b?a-b+H:a-b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
void del(int &a,int b){a=dec(a,b);}
int qpow(int a,int b=H-2){
int res=1;
while(b){if(b&1) res=mul(res,a);a=mul(a,a),b>>=1;}
return res;
}
void init(){
fac[0]=1;
for(int i=1;i<=n;i++) fac[i]=mul(fac[i-1],i);
ifac[n]=qpow(fac[n]);
for(int i=n;i>=1;i--) ifac[i-1]=mul(ifac[i],i);
}
void SOLVE(){
cin>>n>>H,init();
for(int i=1;i<=n;i++) dp[i]=0;
dp[n]=1;
for(int i=n;i>=1;i--){
if(i>1) add(dp[i-1],mul(2*(n-i+1),dp[i]));
if(i>2) del(dp[i-2],mul(mul(n-i+1,n-i+1),dp[i]));
}
for(int i=n,sum1=0,sum2=0;i>=1;i--){
h[i]=mul(mul(fac[n-i+i/2],ifac[n-i]),mul(fac[n-i/2],ifac[n-i]));
g[i]=adc(dec(mul(h[i],dp[i]),sum1),mul(i-1,sum2));
add(sum1,mul(i,g[i])),add(sum2,g[i]);
if(i==1) g[0]=dec(qpow(n,n),sum2);
}
for(int i=0;i<=n;i++) cout<<g[i]<<" \n"[i==n];
}
int main(){
/*2025.4.25 H_W_Y CF2018F3 Speedbreaker Counting (Hard Version) 容斥 + dp + 倒推 dp 系数*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int _;cin>>_;
while(_--) SOLVE();
return 0;
}
AGC034D Manhattan Max Matching
在一个二维坐标系内,点 \((RX_i,RY_i)\) 上有 \(RC_i\) 个红球,点 \((BX_i,BY_i)\) 上有 \(BC_i\) 个蓝球,且保证 \(\sum_{i=1}^n RC_i = \sum_{i=1}^n BC_i\)。
现在要你将这些红球蓝球一一配对,配对的价值为两球所在点之间的曼哈顿距离,请你求出配对完它们的最大价值和。
\(1 \le n \le 1000,RC_i,RB_i \le 10\)。
之前看上这道题是因为题目好像有点牛,结果今天来看一眼发现是傻逼。
做过 U548997 Ynoi Easy Round 2018 T3 星野瑠美衣 那这道题就是砍瓜切菜,只用到了 曼哈顿距离最大值 的 支配性质。
我们同样网络流建图,直接在中间把曼哈顿距离拆成四个点跑费用流即可。
由于题目求的是最大值,所以跑最大流刚好可以把 manhattan 的 \(\max\) 全部支配掉了。
这样就做完了。代码。
AGC014F Strange Sorting
给定 \(1 \sim n\) 排列 \(a\),每次操作会把所有 premax 按顺序放到序列末尾,求多少次操作后序列被排好序。
\(1 \le n \le 2 \times 10^5\)。
神人了。
首先容易发现,每次一定会把 \(n\) 复位,所以总的操作次数不会超过 \(n\),虽然这个东西可能没啥用。
然后考虑 premax 的性质,注意到最有性质的是 \(\min\) 和 \(\max\),因为它们不太会受其它东西影响,而 \(\min\) 它同时不会影响其它的东西。
所以我们考虑从 \(1\) 入手,因为 \(1\) 被选为 premax 当且仅当它在第一个位置,并且 \(1\) 的存在并不会影响其它数是否被选为 premax。
于是考虑删去 \(1\),假设只考虑 \([2,n]\) 这些元素,需要 \(t\) 次才能排序,然后考虑加入 \(1\) 会带来什么影响。
如果我们能把这个做好,那么对于这个问题我们就可以倒着枚举每次处理 \([i,n]\) 对应的答案,那么这个问题就是可以解决的了。
若 \(t=0\),则意味着初始序列合法,那么只需要判断 \(1\) 是否在第一个位置就可以了。
接下来考虑 \(t \gt 0\) 的情况,设 \([2,n]\) 最后一步操作时序列首为 \(f\),容易发现 \(f\gt 2\),因为经过这一次排序之后序列会排好,也就是 \(2\) 会到第一个位置,如果 \(f=2\),那么当前最后一次操作前 \(2\) 在第一个,操作后 \(2\) 也在第一个,就说明最后一次操作是没用的,矛盾。
于是考虑 \(1\) 和 \(f,2\) 的相对位置关系,发现也就只有两种:\((f,1,2)\) 和 \((f,2,1)\)。
(对于 \(1\) 开头的情况,我们可以不需要管他,它和 \((f,2,1)\) 在后续的证明是同一种情况)
发现对于第一种,我们不需要额外的操作,在最后一次操作之后 \(1\) 还是可以到序列开头;而对于第二种,我们就还需要额外的一次操作,把 \(2\) 放到 \(1\) 的后面,这就是需要把 \(t+1\)。
那么现在问题就变成了如何判断 \(f,1,2\) 的相对位置关系,我们希望和初始时的位置关系有一些联系。
结论:当且仅当在初始序列中 \(f,1,2\) 关系与 \((f,1,2)\) 同构,才可以得到 \((f,1,2)\) 的关系(同构意味着可以同构循环移位得到,也就是 \((1,2,f)\) 或 \((2,f,1)\))。
Proof:我们只需要证明在变换过程中 \(f,1,2\) 的相对关系不会变,也就是只有循环移位存在。
首先需要发现 \(f\) 在成为序列开头前没有被操作过,因为如果 \(f\) 被操作过,那么它前面就会出现比他小的元素,于是 \(f\) 永远不可能成为队首了。
进而,如果同构关系改变,只能出现 low,high,low 或者 high,low,high 的情况,我们考虑分情况讨论六种情况
- 初始 \((1,2,f)\),则 \((1,2)\) 一定同时是 premax 或者同时不是,关系不会改变。
- 初始 \((2,1,f)\),这种情况 \(f\) 永远不可能成为最后一次操作的序列开头,因为 \(f\) 一定会在 \(2\) 的后面,不合法。
- 初始 \((1,f,2)\),则 \(f\) 被操作时 \(1\) 也一定会被操作,关系不会改变。
- 初始 \((2,f,1)\),\(f\) 不会成为队首,不合法。
- 初始 \((f,1,2)\),操作时只会操作 \(f\),关系不会改变。
- 初始 \((f,2,1)\),操作时只会操作 \(f\),关系不会改变。
综上我们证明了上面看起来比较显然的结论。
那么这道题就做完了,我们只需要记录一下 \(t,f\) 即可,每次判断一下是否会多用一次额外的操作来复原 \(1\) 就可以了。
时间复杂度线性,关键在于想到删去 \(1\) 来操作。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,a[N],t[N],f[N],p[N];
int main(){
/*2025.4.29 H_W_Y AT_agc014_f [AGC014F] Strange Sorting 排列 + 思维题*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i],p[a[i]]=i;
f[n]=n,t[n]=0;
for(int i=n-1;i>=1;i--){
if(!t[i+1]){
if(p[i]<p[i+1]) t[i]=0,f[i]=i;
else t[i]=1,f[i]=i+1;
}else{
if((p[i]<p[i+1]&&p[i+1]<p[f[i+1]])||(p[f[i+1]]<p[i]&&p[i]<p[i+1])||(p[i+1]<p[f[i+1]]&&p[f[i+1]]<p[i])) f[i]=f[i+1],t[i]=t[i+1];
else t[i]=t[i+1]+1,f[i]=i+1;
}
}
cout<<t[1];
return 0;
}
AGC028E High Elements
你有一个 \(1, 2, \ldots, n\) 的排列 \(P\)。设一个长度为 \(n\) 的 01 字符串 \(S\) 合法,当且仅当,先设两个空序列 \(A\), \(B\),我们按照 \(1\) 到 \(n\) 的顺序,若 \(S\) 当前位为 \(1\) 则把当前位的 \(P\) 添加到序列 \(A\) 的末尾,否则添加到序列 \(B\) 的末尾,使得 \(A\), \(B\) 的前缀最大值个数相等。求字典序最小的合法字符串 \(S\)。
\(n \leq 2 \times 10^5\),\(P\) 是 \(1, 2, \ldots, n\) 的排列。
由于最终要求的是字典序最小的,所以我们考虑贪心。
对于也就是我们需要判断第 \(i\) 位之后,前面 \(X\) 的 premax 个数为 \(C_x\),\(Y\) 序列的 premax 个数为 \(C_y\),并且前面的最大值分别为 \(H_x,H_y\),是否存在一种方案使得序列是好的。
考虑我们把全局的 premax 位置找出来,那么这些位置一定会对答案产生贡献,假设有 \(Q\) 个。
而对于那些非 premax 的位置,我们总有一种方案把它放在前面支配它的 premax 所在序列中,从而不会产生贡献。
于是我们可以假设 \(X,Y\) 其中一个,后面都选全局 premax 位置,因为如果两个序列都存在非 premax 位置,我们可以同时删去两个,不会改变相对差。
所以,我们假设 \(X\) 序列后面都选的全局 premax 位置,对于 \(Y\) 序列同理。
那么假设当前情况下 \(Y\) 选了 \(k\) 个全局 premax,和 \(m\) 个非全局 premax 那么我们需要要求
经过移项可以得到惊人的表达式
而这个式子的右边是一个定值,所以如果我们把全局 premax 的位置权值设为 \(2\),非全局 premax 位置设为 \(1\),相当于我们需要在后面找到一个 上升子序列 使得权值 \(\ge C_x-C_y+Q\)。
然而注意到我们找的是上升的子序列,所以如果存在一种方案能构造出权值 \(=T\) 的,那么就也能构造出 \(=T-2\) 的。
所以我们只需要维护奇数和偶数两种情况的权值最大值就可以了。
于是具体实现的时候,我们先处理出 \(f_{i,0/1}\) 表示 \(i\) 开始的子序列,权值为偶数 / 奇数的最大权值。
那么判断的时候从前往后贪心,预处理的查询都可以通过线段树的区间 max 完成。
这样就做完了,时间复杂度 \(\mathcal O(n \log n)\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5,inf=1e9;
int n,a[N],f[N][2],Cx,Cy,Hx,Hy,cnt=0,ans[N];
bool vis[N];
struct SGT{
#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc
int tr[N<<2];
void pu(int p){tr[p]=max(tr[lc],tr[rc]);}
int qry(int x,int y,int l=1,int r=n,int p=1){
if(x>y) return -inf;
if(x<=l&&y>=r) return tr[p];
if(y<=mid) return qry(x,y,lson);
if(x>mid) return qry(x,y,rson);
return max(qry(x,y,lson),qry(x,y,rson));
}
void chg(int x,int v,int l=1,int r=n,int p=1){
if(l==r) return tr[p]=v,void();
x<=mid?chg(x,v,lson):chg(x,v,rson);pu(p);
}
void build(int l=1,int r=n,int p=1){
tr[p]=-inf;
if(l==r) return ;
build(lson),build(rson);
}
}T[2];
bool chk(int cx,int cy,int hx,int hy){
if(cx-cy+cnt>=0&&T[(cx-cy+cnt)&1].qry(hy+1,n)>=cx-cy+cnt) return true;
if(cy-cx+cnt>=0&&T[(cy-cx+cnt)&1].qry(hx+1,n)>=cy-cx+cnt) return true;
return false;
}
int main(){
/*2025.4.29 H_W_Y AT_agc028_e [AGC028E] High Elements premax + 贪心*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1,mx=0;i<=n;i++){
cin>>a[i];
if(a[i]>mx) mx=a[i],vis[i]=1,++cnt;
}
T[1].build();
for(int i=n;i>=1;i--){
for(int o:{0,1}) f[i][o]=T[o^(!vis[i])].qry(a[i],n)+1+vis[i];
for(int o:{0,1}) T[o].chg(a[i],f[i][o]);
}
for(int i=1;i<=n;i++){
T[0].chg(a[i],0),T[1].chg(a[i],-inf),cnt-=vis[i];
if(chk(Cx+(a[i]>Hx),Cy,max(Hx,a[i]),Hy)) ans[i]=0,Cx+=(a[i]>Hx),Hx=max(Hx,a[i]);
else ans[i]=1,Cy+=(a[i]>Hy),Hy=max(Hy,a[i]);
}
if(Cx!=Cy) cout<<"-1\n",exit(0);
for(int i=1;i<=n;i++) cout<<ans[i];
return 0;
}
AGC053C Random Card Game
给定 \(2n\) 张卡牌,顺次编号为 \(1\) 至 \(2n\)。考虑如下的游戏:
首先,庄家随机地将卡牌分成两堆,每堆 \(n\) 张。每堆内牌的顺序也是随机的。随后,玩家会重复以下操作直到其中一堆为空:
选择一个正整数 \(k\),比较两堆中从上到下第 \(k\) 张卡牌(\(k\) 必须不大于牌堆的大小)。随后,移除两张牌中编号更小的牌。
操作次数即为玩家的得分。假设玩家是一名作弊者,能看到两堆牌中每张牌的编号。玩家将采用最优策略最小化得分,请输出玩家的期望得分在模 \(10^9 + 7\) 意义下的值。
本题中的期望得分是一个分数。假设我们将答案表示为最简分数 \(x/y\),你需要输出的值即为满足 \(y \cdot z \equiv x \pmod{10^9 + 7}\) 的值 \(z\)。
\(n \leq 10^6\)。
首先考虑固定一个两个序列 \(A,B\),那么答案是多少。
我们假设 \(2n\) 在 \(B\) 中,那么显然我们的过程一定是把 \(A\) 消完,因为 \(B\) 中的 \(2n\) 永远消不掉。
发现,记 \(p_i\) 表示 \(B\) 中第一个 \(\gt A_i\) 的位置 \(B\),由于我们需要让这个位置能消 \(A\),所以必须要让 \(p_i \le i\),于是容易发现答案就是 \(n+\max_{i=1}^m (p_i-i)\)。
现在我们考虑如何求这个东西,一个比较简单的思路就是,我们发现答案即为
其中 \(S =\max_{i=1}^m (p_i-i)\),但是要求 \(\max\) 大于一个数并不是很好处理,所以我们考虑转化成 \(\le d\),也就是变成
现在问题就是求 \(P(S \le d)\),发现我们的要求是对于每一个 \(i\),\(B\) 都需要在 \(1 \sim \min(i+d,n)\) 中有一个比他大的。
于是考虑第 \(i\) 个数为最大的概率,由于是随机排列,容易发现概率是 \(\frac 1 {i+\min(i+d,n)}\),所以答案其实是
前面 \(\times 2\) 是因为 \(A,B\) 可以对调。
于是直接维护一下奇数的阶乘和偶数的阶乘就可以了,时间复杂度 \(\mathcal O(n)\)。代码。
Code
void init(){
fac[0]=fac[1]=ifac[0]=ifac[1]=1,inv[1]=1;
for(int i=2;i<=2*n;i++) fac[i]=mul(fac[i-2],i),inv[i]=mul(inv[H%i],H-H/i),ifac[i]=mul(ifac[i-2],inv[i]);
}
int calc(int d){
return mul(2*n-d,mul(mul(ifac[2*n-d],fac[d]),mul(fac[2*n-d-1],ifac[max(d-1,0)])));
}
int main(){
/*2025.4.30 H_W_Y AT_agc053_c [AGC053C] Random Card Game 计数*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n,ans=mul(2*n,n),init();
for(int i=0;i<n;i++) del(ans,calc(i));
cout<<mul(ans,inv[n]);
return 0;
}
AGC030F Permutation and Minimum
有一个 \(2N\) 个数的序列 \(A\),从 \(1\) 到 \(2N\) 标号。你要把 \(1 \sim 2N\) 这些数填进去,使它形成一个排列。
但是已经有一些位置强制填了特定的数了,输入时会给出。
最后令长度为 \(N\) 的序列 \(B\) 为:令 \(B_i = \min\{A_{2i-1}, A_{2i}\}\)。
询问所有方案中能得到的不同的 \(B\) 的数量。
\(1 \leq N \leq 300\)。
这题给人感觉显然可以 dp,我们可以直接把它看成两个数配对的过程,最后把答案乘上 \(cnt!\),其中 \(cnt\) 表示两个都是 -1 的个数。
而涉及到 \(\min\),我们考虑扫值域,从大往小扫(我觉得从小往大也可以),那么容易简单设计 dp,设 \(f_{i,j,k}\) 表示考虑了 \(\ge i\) 的数,当前 \((-1,-1)\) 填了一个的个数是 \(j\),当前 \((-1,x)\) 的组数是 \(k\),这里认为 \((x,y)\) 这种可以删掉。
那么转移考虑当前这个数是否已经确定位置 \(vis_i\),有
- \(vis_i=1\),第一种和一个
-1匹配,放到 \(k\) 中,这一组的 \(\min\) 会是之后的元素,\(f_{i+1,j,k} \to f_{i,j,k+1}\)。 - \(vis_i=1\),将当前位置和之前放在 \(j\) 里面的一个数匹配,\(\min = i\),\(f_{i+1,j,k} \to f_{i,j-1,k}\)。
- \(vis_i=0\),将当前数和其中一个 \(j\) 匹配,我们并不关系 \(j\) 是谁,因为 \(\min\) 反正也取的是 \(i\),\(f_{i+1,j,k} \to f_{i,j-1,k}\)。
- \(vis_i=0\),将当前 \(i\) 和一个 \(k\) 匹配,这里涉及到位置,我们就关心是哪一个 \(x\) 了,\(f_{i+1,j,k} \times k \to f_{i,j,k-1}\)。
- \(vis_i=0\),将当前 \(i\) 放到 \(j\) 里面,等待后面的匹配,\(f_{i+1,j,k} \to f_{i,j+1,k}\)
这样就做完了,直接转移时间复杂度 \(\mathcal O(n^3)\)。代码。
Code
const int N=605,H=1e9+7;
int n,f[2][N][N],a[N],vis[N],cnt=0,o,ans;
int main(){
/*2025.4.30 H_W_Y AT_agc030_f [AGC030F] Permutation and Minimum dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=2*n;i++){
cin>>a[i];
if(a[i]!=-1) vis[a[i]]=1;
}
for(int i=1;i<=n;i++){
if(~a[2*i-1]&&~a[2*i]) vis[a[2*i-1]]=vis[a[2*i]]=2;
cnt+=(!~a[2*i-1]&&!~a[2*i]);
}
f[o=1][0][0]=1;
for(int i=2*n;i>=1;i--){
if(vis[i]==2) continue;
o^=1;
for(int j=0;j<=n;j++) for(int k=0;k<=n;k++) f[o][j][k]=0;
for(int j=0;j<=n;j++) for(int k=0;k<=n;k++){
int t=f[o^1][j][k];
if(j) add(f[o][j-1][k],t);
if(vis[i]) add(f[o][j][k+1],t);
else{
if(k) add(f[o][j][k-1],mul(t,k));
add(f[o][j+1][k],t);
}
}
}
ans=f[o][0][0];
for(int i=1;i<=cnt;i++) ans=mul(ans,i);
cout<<ans;
return 0;
}
AGC038F Two Permutations
给定两个 \(0 \sim (N - 1)\) 的排列 \(\{P_0, P_1, \ldots , P_{N - 1}\}\) 和 \(\{Q_0, Q_1, \ldots , Q_{N - 1}\}\)。
要求构造两个 \(0 \sim (N - 1)\) 的排列 \(\{A_0, A_1, \ldots , A_{N - 1}\}\) 和 \(\{B_0, B_1, \ldots , B_{N - 1}\}\)。
且必须满足条件:
- \(A_i\) 要么等于 \(i\),要么等于 \(P_i\)。
- \(B_i\) 要么等于 \(i\),要么等于 \(Q_i\)。
你需要最大化 \(A_i \ne B_i\) 的下标 \(i\) 的数量,输出这个最大值。
\(1 \le N \le {10}^5\)。
容易发现,对于一个置换环而言,要么 \(A_i=P_i\),要么 \(A_i=i\),对于 \(Q\) 同理。
也就是一个置换环可以选择转(让所有 \(A_i=i\)),或者不转。
那么转 / 不转会带来哪些贡献呢?我们考虑对每一个 \(i\) 分别考虑对置换环的限制,分五种情况讨论
- \(P_i=Q_i=i\),一定无贡献。
- \(P_i=i,Q_i \neq i\),\(Q\) 不转有贡献。
- \(P_i \neq i,Q_i =i\),\(P\) 不转有贡献。
- \(P_i=Q_i,P_i \neq i\),\(P\) 转 \(Q\) 不转 / \(Q\) 转 \(P\) 不转有贡献。
- \(P_i \neq Q_i,P_i \neq i,Q_i \neq i\),\(P,Q\) 不同时转有贡献。
这给人感觉很想最小割,于是我们考虑网络流建模,先把能加的贡献都加上。
对于后面两个限制的建图,我们发现如果把 \(P,Q\) 放到同一侧会非常困难,所以考虑我们建模是,对于 \(P\) 中的置换环和 \(S\) 相连即表示不转,对于 \(Q\) 中的置换环,与 \(T\) 相连即表示不转。
所以建图就是,前三种容易,第四种在 \(P,Q\) 建连双向边,第五种 \(Q \to P\) 连边。
然后跑最小割即可,由于是二分图,时间复杂度 \(\mathcal O(n \sqrt n)\)。代码。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,fa[N],a[N],b[N],p[N],q[N],ans=0;
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
void merge(int u,int v){
u=find(u),v=find(v);
if(u!=v) fa[v]=u;
}
namespace MF{
}
using namespace MF;
int main(){
/*2025.4.30 H_W_Y AT_agc038_f [AGC038F] Two Permutations 最小割*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n,iota(fa+1,fa+n+1,1);
for(int i=1;i<=n;i++) cin>>a[i],++a[i],merge(i,a[i]);
for(int i=1;i<=n;i++) p[i]=find(i);
iota(fa+1,fa+n+1,1);
for(int i=1;i<=n;i++) cin>>b[i],++b[i],merge(i,b[i]);
for(int i=1;i<=n;i++) q[i]=find(i);
S=2*n+1,T=S+1;
for(int i=1;i<=n;i++){
if(a[i]==b[i]&&a[i]==i) continue;
++ans;
if(a[i]==i) add(q[i]+n,T,1);
else if(b[i]==i) add(S,p[i],1);
else if(a[i]==b[i]) add(p[i],q[i]+n,1),add(q[i]+n,p[i],1);
else add(q[i]+n,p[i],1);
}
cout<<ans-dinic();
return 0;
}
AGC050F NAND Tree
一个 \(n\) 个点的树。每个点有点权 \(0\) 或 \(1\)。
每次可以选择一条边,使两个端点缩成一个点,新点权值为两端点权值 AND 再取反。
一直操作直到只剩一个点。
求有多少种操作方案使最终剩下的点权值为 \(1\)。输出答案对 \(2\) 取模的值。
\(2 \le N \le 300\)。
又是一道 \(\bmod 2\) 的好题捏。
首先考虑 \(\bmod 2\) 的简单抵消操作,我们将相邻两次操作配对,如果是 \(n-1\) 是奇数就枚举第一次操作哪一条边。
接下来就假设 \(n-1\) 是偶数,如果 \(2i-1\) 次操作的边和 \(2i\) 次操作的边不相交(即没有公共的端点),那么我们交换这两次操作得到的效果是一样的,所以这样就抵消了。
于是合法的操作序列一定是第 \(2i-1\) 次和第 \(2i\) 次有交,也就是形如 \(x-y-z\)。
显然,如果 \(a_x=a_z\),那么交换两次操作也是不会得到效果的,所以我们知道 \(x,z\) 一定是一个 \(0\) 一个 \(1\)。
然后我们就发现已经不关心 \(y\) 是什么值了,对于当前的一组操作 \(((x,y),(y,z))\) 和 \(((y,z),(x,y))\),发现得到的一定是一个 \(0\) 和一个 \(1\)。
于是我们就记录把这三个点缩成一个点 \(y\),权值是 \(a_y = 0/1\),表示有一种操作得到 \(0\) 有一种得到 \(1\)。
接下来考虑继续合并,继续抵消。
假设当前合并的是 \(x-y-z\),并且 \(y\) 的权值是 \(0/1\),那么发现抵消了!
所以合并时 \(a_y \neq 0/1\),同时如果 \(a_x=0/1\) 并且 \(a_z = 0/1\),你发现方案数也是 \(0\),也就是也抵消了。
这就告诉我们,合并的过程中,不能出现一次合并存在两个 \(0/1\),并且 \(a_y \neq 0/1\)。
所以如果最开始我们操作得到了两个位置是 \(0/1\),那么最终它们一定会在一次和在一起,这样就死了。
于是合并的这个过程只会有一个点是 \(0/1\),也就是第一组操作的 \(y\)。
所以合并过程就跟拓扑序一样了,我们把 \(x\) 设为根,不妨钦定 \(a_x=1\),相当于每次选一个与当前连通块相连的 \((y,z)\),满足 \(y\) 是 \(z\) 的父亲,然后操作 \(x-y-z\),得到一个 \(0/1\) 的节点,并且我们需要第一次的 \(a_z=0\)。
对于第一条限制,每次 \(y\) 是 \(z\) 的父亲,也就是以 \(x\) 为根的拓扑序需要满足第 \(2i-1\) 个是第 \(2i\) 个的父亲,发现如果不是父子关系,则可以交换 \((2i-1,2i)\),也是拓扑关系,所以在 \(\bmod 2\) 的意义下抵消了,我们可以不用管。
而对于第二条限制,则是排除 \(a_x=1,a_z=1\),对于这一种方案,会在 \(x\) 为根算一次,再以 \(z\) 为根算一次,也抵消了。
所以实际上一个一个点 \(x\) 为根对答案的贡献就是树形拓扑序数量,根据经典结论我们知道是
于是维护一下 \(n!\) 的 \(2\) 的次数和 \(sz\) 的 \(2\) 的次数和即可。
每次暴力 dfs 计算 \(size\) 即可做到 \(\mathcal O(n^3)\),可以通过。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define ctz __builtin_ctz
const int N=305;
int n,a[N],c[N],res=0,sz[N],tot=0;
vector<int> G[N];
struct Edge{int u,v;}e[N];
bool ans=0;
void dfs(int u,int fa){
sz[u]=1;
for(int v:G[u]) if(v!=fa) dfs(v,u),sz[u]+=sz[v];
res+=ctz(sz[u]);
}
void solve(){
for(int i=1;i<=n;i++) if(G[i].size()&&a[i]){
res=0,dfs(i,0);
if(res==tot) ans^=1;
}
}
int main(){
/*2025.4.30 H_W_Y AT_agc050_f [AGC050F] NAND Tree Nand + mod 2 + dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n-(n%2==0);i++) tot+=ctz(i);
if(n&1){
for(int i=1,u,v;i<n;i++) cin>>u>>v,G[u].pb(v),G[v].pb(u);
for(int i=1;i<=n;i++) cin>>a[i];
solve();
}else{
for(int i=1;i<n;i++) cin>>e[i].u>>e[i].v;
for(int i=1;i<=n;i++) cin>>c[i];
for(int i=1;i<n;i++){
int u=e[i].u,v=e[i].v;
for(int j=1;j<=n;j++) a[j]=c[j],G[j].clear();
a[u]=!(c[u]&c[v]);
if(n==2) ans^=a[u];
for(int j=1,x,y;j<n;j++) if(j!=i){
x=e[j].u==v?u:e[j].u,y=e[j].v==v?u:e[j].v;
G[x].pb(y),G[y].pb(x);
}
solve();
}
}
cout<<ans;
return 0;
}
AGC041F Histogram Rooks
让我们考虑一个 \(N\) 行 \(N\) 列的正方形棋盘。Arbok切除了棋盘的某些部分使得对于每一个 \(i = 1, 2, 3, \ldots, N\);在第 \(i\) 列中只有自最底部往上数 \(h_i\) 个格子仍存在于棋盘之中。现在,他想把棋子放入到剩余的棋盘中。
車是一种棋子,占据一个方格。如果一个方格所在的同一行或同一列中有一个車而且方格与車之间没有已被切除的格子,那么这个方格就被車所覆盖。特别的,若方格上就是車,那么该方格也被車覆盖。请找出所有可以使剩余的棋盘的全部方格均被
車覆盖的棋子放置方案数。答案可能很大,请对 \(998244353\) 取模。\(1 \leq N \leq 400,1 \leq h_i \leq N\),所有的输入数据均为整数。
首先考虑最基础的基于每一个格子的容斥,去钦定一些格子没有被覆盖,容斥系数是 \((-1)^{|S|}\),那么对应的行和列都不能放车,于是计算出能放车的格子数量 \(cnt\),答案就是 \(2^{cnt}\)。
发现对于这个格子的形态,每一行是分成了若干个连续段,而每一列一定是只有一整段,所以我们考虑用枚举每一列的方式去优化上面的容斥。
这时,我们就枚举 \(S\) 表示,我们钦定的没覆盖的格子所构成的列的集合为 \(S\),那么 \(S\) 这些列是一定不能放车的,那么接下来考虑每一行,这样行列就独立了。
假设对于当前一个长度为 \(len\) 的连续段,它所对应的列在 \(S\) 中有 \(p\) 个,那么放车的方案就是 \(2^{len-p}\)。
但是注意到如果当前连续段中这一行钦定了一个格子,那么这一行就不能放车了,那么钦定格子贡献的容斥系数是
于是看起来一个连续段的贡献就是
但是发现由于我们是枚举的 \(S\),而后面放钦定格子的时候并不能保证每一个 \(S\) 中的列都有钦定格子,所以我们还需要再容斥。
去枚举一个 \(T \subseteq S\),表示钦定了 \(T\) 中没有钦定格子。
于是记 \(q\) 表示当前连续段中在 \(T\) 中的列的数量,那么贡献容易发现就是
如果有多行就直接乘上个几次方就可以了。
而最后还要贡献一个容斥系数 \((-1)^{|T|}\),所以我们现在关心什么,发现只关心 \((-1)^{|T|}\) 以及一个连续段内 \([|S|=|T|]\)(由于 \(S\) 是枚举的,所以 \(S\) 是不存在容斥系数的)。
并且每一个连续段独立,所以这让我们想到建出笛卡尔树进行 dp,每个点表示一个连续段(有可能只有一个点,我们把它设为空节点)。
于是树形 dp 状态是设 \(f_{u,i,0/1}\) 表示在 \(u\) 所表示的连续段中,\(|S|=i\),是否有 \(p=q\) 对应的容斥系数与贡献和。
转移类似于树形背包,十分简单,于是直接做时间复杂度 \(\mathcal O(n^2)\)。代码。
Code
const int N=405,H=998244353;
int n,h[N],sz[N],f[N][N][2],c[N][N][2],idx=0,len[N],ans;
vector<int> G[N];
void init(){
for(int i=0,bas=1;i<=n;i++,bas=mul(bas,2)){
c[i][0][0]=c[i][0][1]=1;
for(int j=1;j<=n;j++) for(int k:{0,1}) c[i][j][k]=mul(c[i][j-1][k],bas-(!k));
}
}
int build(int l,int r,int pre){
int mn=n+1,u=++idx,lst=l;
for(int i=l;i<=r;i++) mn=min(mn,h[i]);
len[u]=mn-pre;
for(int i=l;i<=r;i++){
if(h[i]==mn){
G[u].pb(0);
if(lst<i) G[u].pb(build(lst,i-1,mn));
lst=i+1;
}
}
if(lst<=r) G[u].pb(build(lst,r,mn));
return u;
}
void dfs(int u){
if(!u) return ;
f[u][0][1]=1;
for(int v:G[u]){
dfs(v),sz[u]+=sz[v];
for(int j=sz[u];j>=0;j--){
array<int,2> t={0,0};
for(int i=0;i<=min(sz[v],j);i++)
for(int k1:{0,1}) for(int k2:{0,1})
add(t[k1&k2],mul(f[v][i][k1],f[u][j-i][k2]));
for(int k:{0,1}) f[u][j][k]=t[k];
}
}
for(int i=0;i<=sz[u];i++) for(int k:{0,1})
f[u][i][k]=mul(f[u][i][k],c[sz[u]-i][len[u]][k]);
}
int main(){
/*2025.4.30 H_W_Y AT_agc041_f [AGC041F] Histogram Rooks 容斥 + dp*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>h[i];
init(),build(1,n,0);
sz[0]=1,f[0][1][1]=H-1,f[0][1][0]=f[0][0][1]=1;
dfs(1);
for(int i=0;i<=n;i++) for(int k:{0,1}) add(ans,f[1][i][k]);
cout<<ans;
return 0;
}
UOJ390【UNR #3】百鸽笼
在UOI管理员群里一共有 \(N\) 个管理员,为了查找这些管理员,vfk 准备了 \(N+1\) 个给笼。
为了节省空间,vfk 把这些给笼堆了起来,共有 \(n\) 列,第 \(i\) 列放了 \(a_i\) 个给笼,满足 \(\sum a_i = N+1\)。
每当 UR 结束,管理员们就会按照编号从小到大的顺序回到给笼里,每个管理员回来的时候,会先等概率的在所有还有剩余的给笼的列中随机一个列,然后住到这列剩下的给笼里编号最小的一个中。
现在\(N\)个管理员都回笼了之后,还有一列会空出一个给笼。你能对于每一列,求出这一列有空给笼的概率吗?
\(1 \le n \le 30,1 \le a_i \le 30\)。
这里我们设 \(m = \sum a_i\)。
首先考虑枚举 \(x\),计算最后一个放在 \(x\) 中的答案。
考虑 容斥,有点类似于猎人杀(我还没做过),我们考虑枚举集合 \(S\) 表示钦定 \(S\) 中的元素在 \(x\) 后面被填满,我们计算概率为 \(f(S)\),那么答案是
这样有什么好处呢?
发现由于你钦定了 \(S\) 都在 \(x\) 之后被填满,那么我们就只关心与 \(S\cup x\) 这些东西,而在把 \(x\) 填满的过程中,\(S\) 一定没有满,所以每次填到一个里面的概率都是 \(\frac 1 {|S|+1}\),假设最后把 \(x\) 放满时,大家分别放了 \(b_1,\cdots,b_k\) 个,其中 \(k = |S|\),需要满足 \(b_i \lt a_{S_i}\);同时记录 \(L=\sum b_i+a_x\),于是这里对 \(f(S)\) 的贡献就是
后面是排列的组合数,需要满足最后一个是 \(x\)。
这样一来我们就可以对它 dp 了,这个东西就是类似于背包的东西,我们只关心与个数和和,于是设 \(f_{i,j,k}\) 表示前 \(i\) 个元素选了 \(j\) 个和为 \(k\) 的贡献系数,对于每一个 \(x\) 分别做 dp,时间复杂度 \(\mathcal O(n^6)\) 可以通过。代码。
实际上你做可撤销背包,时间复杂度 \(\mathcal O(n^5)\)。
Code
const int N=35,H=998244353;
int n,a[N],f[2][N][N*N],o=0,sum=0,fac[N*N],ifac[N*N],inv[N*N];
int sgn(int x){return x&1?(H-1):1;}
void init(){
fac[0]=ifac[0]=inv[1]=1;
for(int i=2;i<N*N;i++) inv[i]=mul(inv[H%i],H-H/i);
for(int i=1;i<N*N;i++) fac[i]=mul(fac[i-1],i),ifac[i]=mul(ifac[i-1],inv[i]);
}
int main(){
/*2025.4.30 H_W_Y UOJ #390. 【UNR #3】百鸽笼 容斥 + 背包*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n,init();
for(int i=1;i<=n;i++) cin>>a[i],sum+=a[i];
for(int x=1;x<=n;x++){
for(int i=0;i<=n;i++) for(int j=0;j<=sum;j++) f[0][i][j]=f[1][i][j]=0;
f[o=0][0][0]=1;
for(int i=1;i<=n;i++) if(i!=x){
o^=1;
for(int j=0;j<=i;j++) for(int k=0;k<=sum;k++) f[o][j][k]=0;
for(int j=0;j<=i;j++) for(int k=0;k<=sum;k++) if(f[o^1][j][k]){
int t=f[o^1][j][k];
add(f[o][j][k],t);
for(int l=0;l<a[i];l++) add(f[o][j+1][k+l],mul(t,ifac[l]));
}
}
int res=0;
for(int i=0;i<n;i++) for(int j=0;j<=sum;j++) if(f[o][i][j])
add(res,mul(mul(mul(sgn(i),ifac[a[x]-1]),qpow(inv[i+1],j+a[x])),mul(fac[j+a[x]-1],f[o][i][j])));
cout<<res<<" \n"[x==n];
}
return 0;
}
P5644 [PKUWC2018] 猎人杀
猎人杀是一款风靡一时的游戏“狼人杀”的民间版本,他的规则是这样的:
一开始有 \(n\) 个猎人,第 \(i\) 个猎人有仇恨度 \(w_i\) ,每个猎人只有一个固定的技能:死亡后必须开一枪,且被射中的人也会死亡。
然而向谁开枪也是有讲究的,假设当前还活着的猎人有 \([i_1\ldots i_m]\),那么有 \(\frac{w_{i_k}}{\sum_{j = 1}^{m} w_{i_j}}\) 的概率是向猎人 \(i_k\) 开枪。
一开始第一枪由你打响,目标的选择方法和猎人一样(即有 \(\frac{w_i}{\sum_{j=1}^{n}w_j}\) 的概率射中第 \(i\) 个猎人)。由于开枪导致的连锁反应,所有猎人最终都会死亡,现在 \(1\) 号猎人想知道它是最后一个死的的概率。
答案对 \(998244353\) 取模。
\(1\leq \sum\limits_{i=1}^{n}w_i \leq 100000\)。
考虑容斥,枚举 \(S\) 为在 \(i\) 之后被击毙的人的集合,那么答案就是
那么注意到 \(S \cup 1\) 中,第一次击毙一个人的概率就是 \(w\) 的带权概率,所以答案其实是
于是注意到 \(\sum w_i\) 非常小,我们计算出 \(F(x) = \prod_{i=2}^n (1-x^{w_i})\),于是就可以求出这个东西了。
时间复杂度 \(\mathcal O(\sum w_i \log \sum w_i)\),使用分治 NTT。代码。
Code
poly solve(int l,int r){
if(l==r){
poly now(a[l]+1);
now[0]=1,now[a[l]]=H-1;
return now;
}
poly A=solve(l,mid),B=solve(mid+1,r);
return A*B;
}
int main(){
/*2025.5.2 H_W_Y P5644 [PKUWC2018] 猎人杀 容斥 + 分治 NTT*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n,init();
for(int i=1;i<=n;i++) cin>>a[i];
poly F=solve(2,n);
for(int i=0;i<(int)F.size();i++)
add(ans,mul(F[i],inv[a[1]+i]));
cout<<mul(ans,a[1]);
return 0;
}
CF618F Double Knapsack
给你两个可重集 \(A,B\),\(A,B\) 的元素个数都为 \(n\),它们中每个元素的大小 \(x \in [1,n]\)。请你分别找出 \(A,B\) 的子集,使得它们中的元素之和相等。
\(n \le 10^6\)。
每个数的值域在 \([1,n]\) 就让人很容易想到 鸽巢原理。
假设 \(A_i\) 为 \(A\) 的前缀和,\(B_i\) 为 \(B\) 的前缀和,我们认为 \(A_n \lt B_n\)。
那么对于每一个 \(i\),我们找到 \(B\) 第一个 \(\lt A_i +n\) 的元素 \(p_i\),容易发现一定是有的,去维护这个插值 \(B_{p_i}-A_i\)。
于是对于 \(i \in [0,n]\),有 \(n+1\) 个数,而我们维护的值域只有 \([0,n)\),所以必然有重复。
找到它们即可。代码。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e6+5;
int n,pos[N],to[N];
ll a[N],b[N],sa[N],sb[N];
bool rev=0;
void prt(int la,int ra,int lb,int rb){
if(rev) swap(la,lb),swap(ra,rb);
cout<<(ra-la+1)<<'\n';
for(int i=la;i<=ra;i++) cout<<i<<" \n"[i==ra];
cout<<(rb-lb+1)<<'\n';
for(int i=lb;i<=rb;i++) cout<<i<<" \n"[i==rb];
}
int main(){
/*2025.4.30 H_W_Y CF618F Double Knapsack 构造 + 鸽巢原理*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i],sa[i]=sa[i-1]+a[i];
for(int i=1;i<=n;i++) cin>>b[i],sb[i]=sb[i-1]+b[i];
if(sa[n]>sb[n]) swap(a,b),swap(sa,sb),rev=1;
for(int i=1;i<=n;i++) pos[i]=-1;
for(int i=1;i<=n;i++){
to[i]=to[i-1];
while(sb[to[i]]<sa[i]) ++to[i];
if(~pos[sb[to[i]]-sa[i]]){
int x=pos[sb[to[i]]-sa[i]];
prt(x+1,i,to[x]+1,to[i]),exit(0);
}else pos[sb[to[i]]-sa[i]]=i;
}
cout<<"-1\n";
return 0;
}

浙公网安备 33010602011771号