洛谷P4551 最长异或路径
给定一棵 n 个点的带权树,结点下标从1开始到 n。寻找树中找两个结点,求最长的异或路径。
异或路径指的是指两个结点之间唯一路径上的所有边权的异或。
01trie板子
任意指定一个点为根。建一棵01trie,这棵trie存所有点到根的路径的异或值。
考虑到路径(u,v)的异或=路径(root,u)^(root,v),因此对于每一个点到根的异或值,我们都在01trie上找到另一个点到根的异或值,满足两者异或最大。维护这个最大值。
如何找?高位往下找,能不同就贪心的取不同。因为这样异或值一定尽可能大。
//
// Created by artist on 2021/8/17.
//
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
#define all(x) x.begin(),x.end()
#define DB1(args...) do { cout << #args << " : "; dbg(args); } while (0)
void dbg() { std::cout << " #\n"; }
template<typename T, typename...Args>
void dbg(T a, Args...args) {
std::cout << a << ' ';
dbg(args...);
}
const int maxn = 1e5+5;
vector<pair<int,int> > G[maxn];
int rt[maxn],tot,pos=1,len,trie[maxn<<1][3];
// 计算出每个节点到根的异或和
void dfs(int u,int xorr,int fa){
rt[++tot]=xorr;
for(pair<int,int> v:G[u]){
if(v.first==fa) continue;
dfs(v.first,xorr^v.second,u);
}
}
void Insert(int x){
int p=0;
for(int i=len;~i;--i){
int n=(x>>i)&1;
if(trie[p][n]==0)
trie[p][n]=pos++;
p=trie[p][n];
}
}
signed main() {
int n;scanf("%d",&n);
int mx=0;
for(int i=1,u,v,w;i<=n-1;++i){
scanf("%d%d%d",&u,&v,&w);
G[u].pb(mkp(v,w));
G[v].pb(mkp(u,w));
mx=max(mx,w);
}
len=__lg(mx)+1; // ==3(三位)(0,1,2)
// 预处理出所有(root,u)
dfs(1,0,0);
// 建01trie
for(int i=1;i<=tot;++i){
Insert(rt[i]);
}
ll ans=0;
// 对每一个节点,都在trie找到那个使得他们异或起来最大的和
for(int i=1;i<=n;++i){
int p1=0,p2=0;
ll tmp=0;
for(int j=len;~j;--j){
int k=(rt[i]>>j)&1;
if(trie[p2][k^1])
p2=trie[p2][k^1],tmp|=(ll)(1<<j);
else p2=trie[p2][k];
p1=trie[p1][k];
}
ans=max(ans,tmp);
}
printf("%d\n",ans);
}
CF 683 E Xor Tree
题意:n个点,点有点权。两点间有边当且只当其中一点是另一点在所有点(除自己以外)中异或和最小的点。重边算一条边。问最少删多少个点使得该图是树。
01trie上dp+正难则反(答案问的是最少删除多少,统计的是最多取多少)。
//
// Created by artist on 2021/8/17.
//
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
#define all(x) x.begin(),x.end()
#define DB1(args...) do { cout << #args << " : "; dbg(args); } while (0)
void dbg() { std::cout << " #\n"; }
template<typename T, typename...Args>
void dbg(T a, Args...args) {
std::cout << a << ' ';
dbg(args...);
}
const int maxn = 2e5+5;
int a[maxn];
int trie[maxn<<4][3],sz[maxn<<4];
int len,cnt;
void Insert(int x){
int p=0;
for(int i=len;~i;--i){
int n=(x>>i)&1;
if(!trie[p][n])
trie[p][n]=++cnt;
p=trie[p][n];
sz[p]++;
}
}
int dfs(int u){
if(sz[u]==1) return 1; // 叶子
if(!trie[u][0]) return dfs(trie[u][1]);
if(!trie[u][1]) return dfs(trie[u][0]);
if(sz[trie[u][0]]>=2&&sz[trie[u][1]]>=2) return max(dfs(trie[u][0]),dfs(trie[u][1]))+1;
return dfs(trie[u][0])+dfs(trie[u][1]);
}
signed main() {
int n;scanf("%d",&n);
int mx=0;
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
mx=max(mx,a[i]);
}
len=__lg(mx);
for(int i=1;i<=n;++i){
Insert(a[i]);
}
// dfs回溯处理
printf("%d\n",n-dfs(0));
}
洛谷P6018 [Ynoi2010] Fusion tree
做的第一道由乃题。
01trie处理全局+1和全局异或和。
建多棵trie。
lazy tag缓存修改。
//
// Created by artist on 2021/8/17.
//
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
#define all(x) x.begin(),x.end()
#define DB1(args...) do { cout << #args << " : "; dbg(args); } while (0)
void dbg() { std::cout << " #\n"; }
template<typename T, typename...Args>
void dbg(T a, Args...args) {
std::cout << a << ' ';
dbg(args...);
}
const int maxn = 5e5+5;
const int maxh = 21;
vector<int> G[maxn];
int f[maxn],a[maxn],trie[maxn*23][2],w[maxn*23],xorv[maxn*23],lzy[maxn];
int rt[maxn],cnt; // 根节点
void dfs(int u,int fa){
f[u]=fa;
for(int v:G[u]){
if(v==fa) continue;
dfs(v,u);
}
}
int mknode() {
++cnt;
trie[cnt][1] = trie[cnt][0] = w[cnt] = xorv[cnt] = 0;
return cnt;
}
void maintain(int o){
w[o] = xorv[o] = 0;
if(trie[o][0]){
w[o] += w[trie[o][0]];
xorv[o] ^= xorv[trie[o][0]]<<1;
}
if(trie[o][1]){
w[o] += w[trie[o][1]];
xorv[o] ^= (xorv[trie[o][1]]<<1) | (w[trie[o][1]] & 1);
}
w[o] = w[o] & 1; // 只存奇偶性(删掉这句话也可以,反正有效的只有奇偶性)
}
// 从低位到高位建树
void insert(int &o,int x,int dp){
if(!o) o = mknode();
if(dp>maxh) return (void)(w[o]++);
insert(trie[o][x&1],x>>1,dp+1);
maintain(o);
}
// 删了,但是路径都在。只有w变成了0
void erase(int o,int x,int dp){
if(dp>maxh) return (void)(w[o]--);
erase(trie[o][x&1],x>>1,dp+1);
maintain(o);
}
void add1(int o){
swap(trie[o][1],trie[o][0]);
if(trie[o][0]) add1(trie[o][0]);
maintain(o);
}
// lazy tag 修改
int get(int x){
return (f[x]==-1?0:lzy[f[x]])+a[x];
}
signed main() {
int n,m;scanf("%d%d",&n,&m);
for(int i=1,u,v;i<n;++i){
scanf("%d%d",&u,&v);
G[u].pb(v);
G[v].pb(u);
}
G[0].pb(1);
// get father
dfs(1,-1);
// 建trie
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
if(~f[i]) insert(rt[f[i]],a[i],0);
}
for(int i=1,opt,x,y;i<=m;++i){
scanf("%d",&opt);
if(opt==1){
scanf("%d",&x);
lzy[x]++;
add1(rt[x]); // 每个点的数据仅存在于其根节点的那棵树中
if(x==1) continue;
if(~f[f[x]]) erase(rt[f[f[x]]],get(f[x]),0);
a[f[x]]++;
if(~f[f[x]]) insert(rt[f[f[x]]],get(f[x]),0); // 其根节点要特殊修改(在其根节点的根节点处)
}else if(opt==2){
scanf("%d%d",&x,&y);
if(x!=1) erase(rt[f[x]],get(x),0); // 暴力修改
a[x]-=y;
if(x!=1) insert(rt[f[x]],get(x),0);
}else{
scanf("%d",&x);
printf("%d\n",xorv[rt[x]]^get(f[x]));
}
}
}
ICPC Mid-Central USA Region 2019 Commemorative Race
题意:给一张有向无环图。许多选手都选择最长路走(可能最长路有很多条),要求删掉至多一条边,走到被删的边的一点的选手不得不换路径走。要求选手能走的最长的路最短。
这道题挺有趣。
首先我们考虑把所有最长路径找出来。选手们一定在这上面走。观察发现,当我们把所有最长路径的公共边找出来,每次尝试删除这些边,再于边的起点处跑一次最长路,取最短的删法即是答案。
显然这个时间复杂度不合理,且有许多可以优化的性质没有用上。
因为最长路径的起点很多,我们先建立一个新的点,作为统一的起点,连向所有的点。
我们计算从每个点出发,到达出点的最长路和次长路是多长。
因为删除最长路上的一条边,其实就是从该边起点走次长路。
答案就直接以最长路的路径走到当前点的距离+该点到出点的次长路。即删该边的贡献。
注意到,当一个点往外有分叉时,这条边不能被贡献。
于是我们统计每个边在最长路上出现的次数。
仅仅出现一次的贡献答案即可。
最后减去一因为我们建多了一个点。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5+5;
vector<int> G[maxn];
// 最长路和次长路
int mxlen[maxn],mxlen2[maxn];
int vis[maxn],cnt[maxn];
int ans[maxn];
int dfs1(int x){
if(vis[x]) return mxlen[x];
vis[x]=1;
int res = 0;
for(auto y:G[x]){
res = dfs1(y) + 1;
if(res>mxlen[x]){
mxlen2[x]=mxlen[x];
mxlen[x]=res;
}else if(res>mxlen2[x]){
mxlen2[x]=res;
}
}
return mxlen[x];
}
void dfs2(int x,int len){
if(vis[x]==2) return;
vis[x]=2;
for(auto y:G[x]){
if(mxlen[y]+1==mxlen[x]){
// 最长边
dfs2(y,len+1);
cnt[len]++;
}
}
ans[len]=mxlen2[x]+len;
}
signed main(){
int n,m;scanf("%d%d",&n,&m);
for(int i=1,u,v;i<=m;++i) scanf("%d%d",&u,&v),G[u].push_back(v);
for(int i=1;i<=n;++i) G[0].push_back(i);
dfs1(0);
dfs2(0,0);
int res = mxlen[0];
for(int i=1;i<=n;++i){
if(cnt[i]==1) res = min(res,ans[i]);
}
printf("%d\n",res-1);
}
ICPC Mid-Central USA Region 2019 Sum and Product
题意:给一个序列,问有多少个区间,区间乘积等于加和。n=2e5。
鸽巢发现只有1特殊,会贡献加和但不贡献乘积。满足条件的区间的非1数个数不超过30个。
枚举区间右端,缩1,向左跳。
//
// Created by artist on 2021/8/19.
//
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
#define all(x) x.begin(),x.end()
#define DB1(args...) do { cout << #args << " : "; dbg(args); } while (0)
void dbg() { std::cout << " #\n"; }
template<typename T, typename...Args>
void dbg(T a, Args...args) {
std::cout << a << ' ';
dbg(args...);
}
const int maxn = 2e5+5;
int lst[maxn],a[maxn]; // 当前位置上一个非1的位置
signed main() {
int n;scanf("%d",&n);
for(int i=1,l=0;i<=n;++i){
scanf("%d",&a[i]);
lst[i]=l;
if(a[i]!=1) l=i;
}
int ans=0;
// 枚举右端点
for(int i=1;i<=n;++i){
ll sm=0,prod=1;
int j=i;
while(j){
sm += a[j];
prod *= a[j];
if(prod>=1e8) break;
if(sm<=prod && sm+j-lst[j]-1>=prod) {
ans++;
}
sm+=j-lst[j]-1; // 1的个数
j=lst[j];
}
}
printf("%d\n",ans-n);
}
2021CCPC华为云挑战赛1006 仓颉造数
对于已有的
2
k
2^k
2k 个数,设为a1,a2,…,
a
2
k
a_{2^k}
a2k,先两两平均,得到
a
1
+
a
2
2
,
a
3
+
a
4
2
,
.
.
.
,
a
2
k
−
1
+
a
2
k
2
\frac{a_1+a_2}{2},\frac{a_3+a_4}{2},...,\frac{a_{2^k-1}+a_{2^k}}{2}
2a1+a2,2a3+a4,...,2a2k−1+a2k,再两两平均,化开后可以发现得到
即
∑
a
i
2
k
\frac{\sum a_i}{2^k}
2k∑ai。
a个
a
b
\frac{a}{b}
ba和b个
b
a
\frac{b}{a}
ab,共a+b(
2
k
2^k
2k)个数,他们进行操作后可达1.
代码略
2021牛客多校2 B - Cannon
这个sb本来还在用ntt冲反正一定会TLE
子问题1
2 k ∑ k ! i ! ( k − i ) ! n ! ( n − i ) ! m ! ( m − ( k − i ) ) ! 2^k\sum \frac{k!}{i!(k-i)!}\frac{n!}{(n-i)!}\frac{m!}{(m-(k-i))!} 2k∑i!(k−i)!k!(n−i)!n!(m−(k−i))!m!
= 2 k k ! ∑ ( n i ) ( m k − i ) =2^k k!\sum \binom{n}{i}\binom{m}{k-i} =2kk!∑(in)(k−im)
= 2 k k ! ( n + m k ) =2^k k!\binom{n+m}{k} =2kk!(kn+m)(*)
子问题2
2 k ∑ n ! ( n − i ) ! m ! ( m − ( k − i ) ) ! 2^k\sum \frac{n!}{(n-i)!}\frac{m!}{(m-(k-i))!} 2k∑(n−i)!n!(m−(k−i))!m!
= 2 k ∑ n ! m ! ( n + m − k ) ! ( n + m − k ) ! ( n − i ) ! ( m − ( k − i ) ) ! =2^k \sum \frac{n!m!}{(n+m-k)!}\frac{(n+m-k)!}{(n-i)!(m-(k-i))!} =2k∑(n+m−k)!n!m!(n−i)!(m−(k−i))!(n+m−k)!
= 2 k n ! m ! ( n + m − k ) ! ∑ ( n + m − k ) ! ( n − i ) ! ( m − ( k − i ) ) ! =2^k \frac{n!m!}{(n+m-k)!}\sum \frac{(n+m-k)!}{(n-i)!(m-(k-i))!} =2k(n+m−k)!n!m!∑(n−i)!(m−(k−i))!(n+m−k)!
= 2 k n ! m ! ( n + m − k ) ! ∑ n − k n ( n + m − k ) ! i ! ( n + m − k − i ) ! =2^k \frac{n!m!}{(n+m-k)!}\sum_{n-k}^n \frac{(n+m-k)!}{i!(n+m-k-i)!} =2k(n+m−k)!n!m!∑n−kni!(n+m−k−i)!(n+m−k)!(*变换枚举下标)
= 2 k n ! m ! ( n + m − k ) ! ∑ n − k n ( n + m − k i ) =2^k \frac{n!m!}{(n+m-k)!}\sum_{n-k}^n \binom{n+m-k}{i} =2k(n+m−k)!n!m!∑n−kn(in+m−k)
注意到, ∑ n − k n ( n + m − k i ) = ∑ n ( n + m − k i ) − ∑ n − k − 1 ( n + m − k i ) \sum_{n-k}^{n}\binom{n+m-k}{i}=\sum^{n}\binom{n+m-k}{i}-\sum^{n-k-1}\binom{n+m-k}{i} ∑n−kn(in+m−k)=∑n(in+m−k)−∑n−k−1(in+m−k),表现为组合数前缀和的形式。
考虑化简
∑ 0 ≤ i ≤ n ( n + m − k i ) − ∑ 0 ≤ i ≤ n − k − 1 ( n + m − k i ) \sum_{0\leq i\leq n}\binom{n+m-k}{i}-\sum_{0\leq i\leq n-k-1}\binom{n+m-k}{i} ∑0≤i≤n(in+m−k)−∑0≤i≤n−k−1(in+m−k)
= ∑ 0 ≤ i ≤ n ( n + m − k i ) − ∑ 0 ≤ i ≤ n − k − 1 ( n + m − k n + m − k − i ) =\sum_{0\leq i\leq n}\binom{n+m-k}{i}-\sum_{0\leq i\leq n-k-1}\binom{n+m-k}{n+m-k-i} =∑0≤i≤n(in+m−k)−∑0≤i≤n−k−1(n+m−k−in+m−k)
= ∑ 0 ≤ i ≤ n ( n + m − k i ) − ∑ m + 1 ≤ i ≤ n + m − k ( n + m − k i ) =\sum_{0\leq i\leq n}\binom{n+m-k}{i}-\sum_{m+1\leq i\leq n+m-k}\binom{n+m-k}{i} =∑0≤i≤n(in+m−k)−∑m+1≤i≤n+m−k(in+m−k)
= ∑ 0 ≤ i ≤ n ( n + m − k i ) − ( ∑ 0 ≤ i ≤ n + m − k ( n + m − k i ) − ∑ 0 ≤ i ≤ m ( n + m − k i ) ) =\sum_{0\leq i\leq n}\binom{n+m-k}{i}-(\sum_{0\leq i\leq n+m-k}\binom{n+m-k}{i}-\sum_{0\leq i\leq m}\binom{n+m-k}{i}) =∑0≤i≤n(in+m−k)−(∑0≤i≤n+m−k(in+m−k)−∑0≤i≤m(in+m−k))
= ∑ 0 ≤ i ≤ n ( n + m − k i ) + ∑ 0 ≤ i ≤ m ( n + m − k i ) − 2 n + m − k =\sum_{0\leq i\leq n}\binom{n+m-k}{i}+\sum_{0\leq i\leq m}\binom{n+m-k}{i}-2^{n+m-k} =∑0≤i≤n(in+m−k)+∑0≤i≤m(in+m−k)−2n+m−k 设为 s u m [ n + m − k ] sum[n+m-k] sum[n+m−k]。
对于连续的k,组合数前缀和可以进行递推。
考虑设 S ( n , m ) = ∑ i = 0 m ( n i ) S(n,m)=\sum_{i=0}^{m}\binom{n}{i} S(n,m)=∑i=0m(in)
有以下trick:
S ( n , m + 1 ) = S ( n , m ) + ( n m + 1 ) S(n,m+1)=S(n,m)+\binom{n}{m+1} S(n,m+1)=S(n,m)+(m+1n), S ( n , m − 1 ) = S ( n , m ) − ( n m ) S(n,m-1)=S(n,m)-\binom{n}{m} S(n,m−1)=S(n,m)−(mn)
S ( n + 1 , m ) = ∑ i = 0 m ( n + 1 i ) = ∑ i = 0 m ( n i ) + ( n i − 1 ) = 2 S ( n , m ) − ( n m ) S(n+1,m)=\sum_{i=0}^m \binom{n+1}{i}=\sum_{i=0}^m\binom{n}{i}+\binom{n}{i-1}=2S(n,m)-\binom{n}{m} S(n+1,m)=∑i=0m(in+1)=∑i=0m(in)+(i−1n)=2S(n,m)−(mn)
因此,我们可以递推出 s u m sum sum。
结束。
//
// Created by artist on 2021/8/24.
//
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
#define all(x) x.begin(),x.end()
#define DB1(args...) do { cout << #args << " : "; dbg(args); } while (0)
void dbg() { std::cout << " #\n"; }
template<typename T, typename...Args>
void dbg(T a, Args...args) {
std::cout << a << ' ';
dbg(args...);
}
const int maxn = 5e6+5;
const int mod = 1e9+9;
int fac[maxn<<1],invf[maxn<<1];
int qpow(int a,int n){
ll ans=1;
while(n){
if(n&1) ans=1ll*a*ans%mod;
a=1ll*a*a%mod;
n>>=1;
}
return ans;
}
void init(int n,int m){
fac[0]=1;
for(int i=1;i<=n+m+5;++i) fac[i]=1ll*fac[i-1]*i%mod;
invf[n+m+5]=qpow(fac[n+m+5],mod-2);
for(int i=n+m+4;i>=0;--i) invf[i]=1ll*invf[i+1]*(i+1)%mod;
}
int C(int n,int m){
if(n<m||m<0) return 0;
return 1ll*fac[n]*invf[m]%mod*invf[n-m]%mod;
}
int S[maxn<<1];
signed main() {
int x,y;scanf("%d%d",&x,&y);
x-=2,y-=2;
init(x,y);
int k2=1,ans1=0,ans2=0;
S[0]=1;
for(int i=1;i<=x+y;++i){
S[i]=2ll*S[i-1]%mod;
S[i]=(1ll*S[i]-C(i-1,x)+mod)%mod;
S[i]=(1ll*S[i]-C(i-1,y)+mod)%mod;
}
for(int i=0;i<=x+y;++i){
ans1 ^= (1ll*k2*fac[i]%mod*C(x+y,i))%mod;
ans2 ^= (1ll*k2*fac[x]%mod*fac[y]%mod*invf[x+y-i]%mod*S[x+y-i])%mod;
k2=2ll*k2%mod;
}
printf("%d %d\n",ans1,ans2);
}
2021牛客4 G - product
题目要求 ∑ a i ≥ 0 , ∑ a i = D D ! ∏ i = 1 n ( a i + k ) ! \sum_{a_i\geq 0,\sum a_i=D}\frac{D!}{\prod_{i=1}^n(a_i+k)!} ∑ai≥0,∑ai=D∏i=1n(ai+k)!D!。( n ≤ 50 , k ≤ 50 , D ≤ 1 0 8 n\leq 50,k\leq 50,D\leq 10^8 n≤50,k≤50,D≤108)
我们先考虑一个更一般的式子, ∑ a i ≥ 0 , ∑ a i = D D ! ∏ i = 1 n a i ! \sum_{a_i\geq 0,\sum a_i=D}\frac{D!}{\prod_{i=1}^na_i!} ∑ai≥0,∑ai=D∏i=1nai!D!
展开累乘符号, = ∑ a i ≥ 0 , ∑ a i = D D ! a 1 ! ⋅ a 2 ! ⋅ . . . ⋅ a n ! =\sum_{a_i\geq 0,\sum a_i=D}\frac{D!}{a_1!\cdot a_2!\cdot ...\cdot a_n!} =∑ai≥0,∑ai=Da1!⋅a2!⋅...⋅an!D!。
用组合数学的眼光,这可以视作将 D D D 个不同的球放入 n n n 个盒子(盒子可以为空)的方案数。
因此上式 = D n =D^n =Dn。(每个球有 n n n 个盒子可以放)
于是有结论 ∑ a i ≥ 0 , ∑ a i = D ∏ i 1 a i ! = n D D ! \sum_{a_i\geq 0,\sum a_i=D}\prod_i \frac{1}{a_i!}=\frac{n^D}{D!} ∑ai≥0,∑ai=D∏iai!1=D!nD (*)
我们把原式 ∑ a i ≥ 0 , ∑ a i = D D ! ∏ i = 1 n ( a i + k ) ! \sum_{a_i\geq 0,\sum a_i=D}\frac{D!}{\prod_{i=1}^n(a_i+k)!} ∑ai≥0,∑ai=D∏i=1n(ai+k)!D! 转化为 ∑ a i ≥ k , ∑ a i = D + n k ∏ i = 1 n D ! a i ! \sum_{a_i\geq k,\sum a_i=D+nk}\prod_{i=1}^n \frac{D!}{a_i!} ∑ai≥k,∑ai=D+nk∏i=1nai!D!
= D ! ( D + n k ) ! ⋅ ∑ a i ≥ k , ∑ a i = D + n k ∏ i = 1 n ( D + n k ) ! a i ! =\frac{D!}{(D+nk)!}\cdot \sum_{a_i\geq k,\sum a_i={D+nk}}\prod_{i=1}^n \frac{(D+nk)!}{a_i!} =(D+nk)!D!⋅∑ai≥k,∑ai=D+nk∏i=1nai!(D+nk)!
我们现在的问题转化为:将 D + n k D+nk D+nk 个不同的球放入 n n n 个盒子里,每个盒子至少放 k k k 个球的方案数,再乘上一个修正系数 C = D ! ( D + n k ) ! C=\frac{D!}{(D+nk)!} C=(D+nk)!D! 。
考虑容斥。
考虑容斥的公式:
定义 A i = { x : x 属 于 S 且 x 具 有 性 质 P i } A_i=\{x:x属于S且x具有性质P_i\} Ai={x:x属于S且x具有性质Pi}
∣ A ˉ 1 ∩ A ˉ 2 ∩ ⋯ ∩ A ˉ n ∣ = ∣ S ∣ − ∑ ∣ A i ∣ + ∑ ∣ A i ∩ A j ∣ − ∑ ∣ A i ∩ A j ∩ A k ∣ + ⋯ + ( − 1 ) n ∣ A 1 ∩ A 2 ∩ ⋯ ∩ A n ∣ |\bar A_1\cap \bar A_2\cap \dots \cap \bar A_n|=|S|-\sum |A_i|+\sum |A_i\cap A_j|-\sum |A_i \cap A_j \cap A_k|+\dots +(-1)^n|A_1\cap A_2 \cap \dots \cap A_n| ∣Aˉ1∩Aˉ2∩⋯∩Aˉn∣=∣S∣−∑∣Ai∣+∑∣Ai∩Aj∣−∑∣Ai∩Aj∩Ak∣+⋯+(−1)n∣A1∩A2∩⋯∩An∣
那么我们的式子也形如
C ∑ i = 0 n ( − 1 ) i ∑ ∣ A j 1 ∩ A j 2 ∩ ⋯ ∩ A j i ∣ C \sum_{i=0}^n(-1)^i\sum |A_{j1}\cap A_{j2}\cap \dots \cap A_{ji}| C∑i=0n(−1)i∑∣Aj1∩Aj2∩⋯∩Aji∣
其中 A i A_i Ai 表示 a i < k a_i<k ai<k 的sequence的集合。
定义 d p i , j dp_{i,j} dpi,j 为把 j j j 个球分为 i i i 个非法组的方案数。
转移为:
d p i , j = ∑ s = 0 k − 1 d p i − 1 , j − s ( j s ) dp_{i,j}=\sum_{s=0}^{k-1}dp_{i-1,j-s}\binom{j}{s} dpi,j=∑s=0k−1dpi−1,j−s(sj)
含义为在这j个中选出s个剔除,剩下的从前一个状态中转移而来。
我们的式子由此可以写成
C ∑ i = 0 n ( − 1 ) i ( n i ) ∑ j = 0 i ( k − 1 ) d p i , j ( D + n k j ) ( n − i ) D + n k − j C\sum_{i=0}^n(-1)^i\binom{n}{i}\sum_{j=0}^{i(k-1)}dp_{i,j}\binom{D+nk}{j}(n-i)^{D+nk-j} C∑i=0n(−1)i(in)∑j=0i(k−1)dpi,j(jD+nk)(n−i)D+nk−j
其中, ( n i ) \binom{n}{i} (in) 表示n个中选出i个非法盒子的方案数。(可以这样做的原因是,不同的Ai(例如:A_i和A_j的计算方式本质相同)等价)
∑ j = 0 i ( k − 1 ) \sum_{j=0}^{i(k-1)} ∑j=0i(k−1) 表示枚举多少个球放进这i个非法盒子(最多放i(k-1)个,因为是非法(少于k))。
( D + n k j ) \binom{D+nk}{j} (jD+nk) 表示从所有D+nk个球中选出这j个的方案数。
( n − i ) D + n k − j (n-i)^{D+nk-j} (n−i)D+nk−j 表示除了这j个球以外的球,他们可以在剩下的盒子中乱放。(可能乱放完了之后剩下的盒子也存在不合法的,但这无所谓,因为我们在容斥。我们只需要指定的那i个盒子非法即可)
//
// Created by Artist on 2021/8/26.
//
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 55;
const int mod = 998244353;
int qpow(int a,int n){
ll ans=1;
while(n){
if(n&1) ans=ans*a%mod;
a=1ll*a*a%mod;
n>>=1;
}
return ans;
}
int C1[maxn*maxn][maxn]; // combine
int dp[maxn][maxn*maxn]; // dp
int main(){
int n,k,d;scanf("%d%d%d",&n,&k,&d);
for(int i=0;i<=n*k;++i) C1[i][0]=1;
for(int i=1;i<=n*k;++i){
for(int j=1;j<=min(max(k,n),i);++j){
C1[i][j]=(1ll*C1[i-1][j-1]+C1[i-1][j])%mod;
}
}
dp[0][0]=1;
for(int i=0;i<n;++i){
for(int j=0;j<=i*(k-1);++j){
for(int s=0;s<=k-1;++s){
dp[i+1][j+s]=(1ll*dp[i+1][j+s]+1ll*dp[i][j]*C1[j+s][s])%mod;
}
}
}
int ans=0;
for(int i=0;i<=n;++i){
int sm=0;
int fac=1,cc=(i&1?mod-C1[n][i]:C1[n][i]),x=d+n*k,y=1;
for(int j=0;j<=i*(k-1);++j){
sm=(1ll*sm+1ll*cc*dp[i][j]%mod*fac%mod*qpow(n-i,d+n*k-j)%mod)%mod;
fac=1ll*fac*x%mod*qpow(y,mod-2)%mod;
y++;x--;
}
ans=(ans+sm)%mod;
}
for(int i=d+n*k;i>d;--i) ans=1ll*ans*qpow(i,mod-2)%mod;
printf("%d\n",ans);
}