ABC253
F - Operations on a Matrix(树状数组)
Problem
现在有一个\(N\times M\)大小的矩阵,一开始全部元素都是\(0\),现在进行\(Q\)次操作,每次操作有\(3\)中类型
1 l r x
:把第\(l\)到第\(r\)列的所有元素加上\(x\)2 i x
:把第\(i\)行的元素全部替换成\(x\)3 i j
:输出\((i,j)\)处的元素
\(1\le N,M,Q\le 2\times 10^5\),\(1\le x\le 10^9\)
Solve
把行和列分开维护。对于列来说,我们全局维护,操作 1 对列来说相当于区间同时加上一个数,这个可以用树状数组来维护。对于行来说,由于每进行一次操作 2, 之前列对当前行的影响都会消除,所以行用来维护答案的变化。具体就是:
- 对于操作 1 我们不考虑后面的情况,而是每次遇到操作 1 就区间全部加上一个数。
- 对于操作 2,我们遍历最近的以这行作为\(i\)的询问(该询问前面的操作 2 是这次的操作,并且没有其他行操作),然后去统计列的影响。
- 对于操作 3,直接用该询问的列的全局的贡献+该询问的行的最新的一次变化的贡献-该询问的行之前列的贡献
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e5+10;
ll tr[N];
int n,m;
void add(int p,int x)
{
for(;p<=m+1;p+=p&-p) tr[p]+=x;
}
ll ask(int p)
{
ll res=0;
for(;p;p-=p&-p) res+=tr[p];
return res;
}
int t[N],l[N],r[N],x[N],lastest[N],pre[N];
ll cx[N];
vector<int>g[N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin>>n>>m>>q;
for(int i=1;i<=q;i++)
{
cin>>t[i];
if(t[i]==1) cin>>l[i]>>r[i]>>x[i];
else if(t[i]==2) cin>>r[i]>>x[i],lastest[r[i]]=i;
else cin>>l[i]>>r[i],g[lastest[l[i]]].push_back(i),pre[i]=lastest[l[i]];
}
for(int i=1;i<=q;i++)
{
if(t[i]==1) add(l[i],x[i]),add(r[i]+1,-x[i]);
else if(t[i]==2)
{
for(auto j:g[i]) cx[j]=ask(r[j]);
}else cout<<ask(r[i])+x[pre[i]]-cx[i]<<'\n';
}
return 0;
}
ABC 213 G - Connectivity 2
Problem
给定一个简单无向图\(G\),该图有\(N\)个顶点和\(M\)条边。考虑移除一些边从图\(G\)中(可以不移除),这样产生的新图\(H\)有\(2^M\)种。对于每个点\(k=2,3,...,N\),计算有多少个新图\(H\)保证点\(1\)和\(k\)是连通的。答案对\(998244353\)取模
\(1\le N\le 17\),\(1\le M\le \frac{N(N-1)}{2}\)
Solve
黑科技(Fast Zeta Transform)
定义\(f(S)\)表示点集\(S\)构成的\(G\)的连通子图的数量,\(g(S)\)表示是为点集\(S\)构成的\(G\)的子图的数量,\(cnt(S)\)点集\(S\)在原图\(G\)中含有的边的数量。
容易得到\(g(S)=2^{cnt(S)}\),每条边可选可不选
由容斥原理容易得到\(f(S)=g(S)-\sum_{v\in T\ T\subseteq S}f(T)g(S/T)\)。前面的\(g(S)\)容易理解是既包含了连通的子图也包含了不连通的子图,后面就是钦定某个点\(v\)是属于连通块部分的,即\(v\)要在\(T\)中,然后根据乘法原理计算即可。
对于点\(k\)来说,答案就是\(res=\sum_{1,k\in S}f(S)g(V-S)\),其中\(V\)是整个点集,点集\(S\)要包含点\(1\)和\(k\)
实现的一些具体细节:
-
枚举子集\(T\)的时候是枚举真子集,这里可以用类似
lowbit
的方法快速枚举子集 -
钦定\(v\)的时候可以任意钦定,因为后面的\(g(S/T)\)实际上是会包含连通部分的,如果每次枚举哪个点\(v\),会算重,所以任意枚举一个,这里枚举\(S\)中最低位的点作为\(v\)。
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int mod= 998244353;
ll power(ll x ,int y)
{
ll res=1;
while(y)
{
if(y&1) res=res*x%mod;
x=x*x%mod;
y>>=1;
}
return res;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m;
cin>>n>>m;
vector<pair<int,int>>e(m);
for(int i=0;i<m;i++)
{
int u,v;
cin>>u>>v;
u--,v--;
e[i]={u,v};
}
vector<int>cnt(1<<n);
vector<ll>f(1<<n),ans(n);
for(int s=0;s<(1<<n);s++)
{
for(auto [u,v]:e)
if(((s>>u)&1)&&((s>>v)&1)) cnt[s]++;
}
for(int s=0;s<(1<<n);s++)
{
f[s]=power(2,cnt[s]);
for(int t=s&(s-1);t;t=(t-1)&s)
{
if(!(t&(s&-s))) continue;
//if(!(t&(1<<(__lg(s))>>))) continue;
f[s]=(f[s]-f[t]*power(2,cnt[s-t])%mod+mod)%mod;
}
if(s&1)
{
for(int j=1;j<n;j++)
{
if((s>>j)&1) ans[j]=(ans[j]+f[s]*power(2,cnt[(1<<n)-s-1])%mod)%mod;
}
}
}
for(int i=1;i<n;i++) cout<<ans[i]<<"\n";
}
Ex- We Love Forest
Problem
有一个\(N\)个点的图\(G\),一开始这个图里面没有边,但有\(M\)无向边条边(编号从\(1\)到\(m\))可以让你加入到图中。现在你可以做如下操作\(N-1\)次,每次你可以随机地选择一条边,并把这边加入到图中,(加过的边还可以再加,所以可以出现重边)。现在问对于\(K=1,2,...,N-1\),在\(K\)次操作之后\(G\)是一个森林的概率。答案对\(998244353\)取模
\(1\le N\le 14\),\(1\le M\le 500\)
Solve
这个题和上面的\(G\)有异曲同工之秒,所以先写了。
一些定义
- \(f(S):=\)点集\(S\)可以构成的树的数量
- \(g_i(S):=\)点集\(S\)可以构成的森林并且含有\(i\)条边的数量
那么对于每一个\(K\),答案就是\(\frac{g_k(V)k!}{m^k}\),其中\(V\)是整个点集
对于\(f(S)\),在已知点集的情况下,可以知道图的构成情况,问题就是求一个无向图的生成树的数量,这个可以用矩阵树定理(基尔霍夫定理)来求解。时间复杂度是\(O(2^nn^3)\)。矩阵树定理。
对于\(g_i(S)\)的计算,和上面的\(G\)差不多,即\(g_i(S)=\sum_{v\in T\ T\subseteq S}f(T)g_{i-|T|+1}(S/T)\),之所以是\(i-|T|+1\),是因为\(T\)这个点集是构成一棵树,一棵树有\(|T|-1\)条边。
树的集合可以看做森林,所以这里枚举树
注意点
- 这里初始状态时\(g[0][0]=1\),注意这里递推的时候要从小集合递推到大集合,不能直接枚举大集合的子集。
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=998244353;
const int eps=1e-9;
ll power(ll x,int y)
{
ll res=1;
while(y)
{
if(y&1) res=res*x%mod;
x=x*x%mod;
y>>=1;
}
return res;
}
ll cal_det(vector<vector<ll>>a,int n)
{
ll det=1;
for(int i=0;i<n;i++)
{
int k=i;
for(int j=i+1;j<n;j++)
if(abs(a[j][i])>abs(a[k][i])) k=j;
if(a[k][i]==0){
det=0;
break;
}
swap(a[i],a[k]);
if(i!=k) det=mod-det;
det=det*a[i][i]%mod;
for(int j=i+1;j<n;j++) a[i][j]=a[i][j]*power(a[i][i],mod-2)%mod;
for(int j=0;j<n;j++)
if(j!=i&&a[j][i]!=0)
for(int k=i+1;k<n;k++) a[j][k]=(a[j][k]-a[i][k]*a[j][i])%mod;
}
if(det<0) det=det+mod;
return det;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m;
cin>>n>>m;
vector<vector<int>>e(n,vector<int>(n));
for(int i=0;i<m;i++)
{
int u,v;
cin>>u>>v;
u--,v--;
e[u][v]++;
e[v][u]++;
}
vector<ll>f(1<<n);
for(int s=0;s<(1<<n);s++)
{
vector<int>p;
for(int j=0;j<n;j++)
if((s>>j)&1) p.push_back(j);
int len=p.size();
vector<vector<ll>>a(len,vector<ll>(len));
for(int i=0;i<len;i++)
for(int j=i+1;j<len;j++)
{
a[i][i]+=e[p[i]][p[j]];
a[j][j]+=e[p[i]][p[j]];
a[i][j]=(a[i][j]+mod-e[p[i]][p[j]])%mod;
a[j][i]=(a[j][i]+mod-e[p[i]][p[j]])%mod;
}
f[s]=cal_det(a,len-1);
}
vector<vector<ll>>g(n+1,vector<ll>(1<<n));
g[0][0]=1;
for(int i=0;i<n;i++)
{
for(int s=0;s<(1<<n);s++)
{
if(g[i][s]==0) continue;
int ns=((1<<n)-1)^s;
//ns是s的补集,枚举补集的子集,从而推出大集合。
for(int t=ns;t;t=(t-1)&ns)
{
int j=__builtin_popcount(t);//统计t中1的个数,计算|t|
if(!(t&(ns&-ns))) continue; //钦定一个v
g[i+j-1][s+t]=(g[i+j-1][s+t]+f[t]*g[i][s]%mod)%mod;
//小集合递推到大集合
}
}
}
vector<ll>fac(n+1);
fac[0]=1;
for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod;
for(int i=1;i<n;i++)
{
ll res=fac[i]*g[i][(1<<n)-1]%mod*power(power(m,i),mod-2)%mod;
cout<<res<<'\n';
}
}