容斥做題記錄(早期)
平邑一中集訓被容斥 dp 和數位 dp 吊起來打
打算回來補補 dp
結果是個神仙數學題
看到題一開始以為是個儀仗隊
後來才發現 \(i\) 和 \(j\) 限制不同,尤拉函式不能一下切掉
看了題解之後才知道是容斥題
求
\[\sum_{i=1}^n\sum_{j=1}^mgcd(i,j) \]可以考慮設 \(g[x]\) 為能夠被x整除的二元組 \((i,j)\) 的個數
那麼顯然,$$g[x]=\left\lfloor\frac{n}{x}\right\rfloor\times\left\lfloor\frac{m}{x}\right\rfloor$$
設 \(f[x]\) 為最大公因數為 \(x\) 的二元組個數,這玩意不好求
考慮容斥
\[f[x]=g[x] - \sum_{i=2}^{\left\lfloor\frac{min(n,m)}{x}\right\rfloor} \]然後f就變得可求了
我似乎在 day10 的 T1 裡面想到過類似的東西
複雜度大概是調和級數\(O(n\log n\log n)\)
#include<bits/stdc++.h> using namespace std; #define int long long #define INF 1ll<<30 #define ill unsigned long long template<typename _T> inline void read(_T &x) { x=0;char s=getchar();int f=1; while(s<'0'||s>'9') {f=1;if(s=='-')f=-1;s=getchar();} while('0'<=s&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();} x*=f; } const int np = 1e5 + 5; int f[np]; signed main() { int n,m; read(n); read(m); for(int x = min(n , m);x;x--) { f[x] += (n/x) * (m/x); for(int i=2;x * i <= min(n , m);i++) f[x] -= f[i * x]; } int Ans= 0 ; for(int i=1;i<=min(n,m);i++) { Ans += f[i] * i; } Ans*=2; Ans -=m * n; cout<<Ans; }
這是個比整除分塊更優的儀仗隊解法
如果只有一個烏龜,則是經典的格路計數問題。
考慮一個合法的方案可能是$$(1,2)->(n-1,m),(2,1)->(n,m-1)$$
不合法的方案必定是$$(1,2)->(n,m-1),(2,1)->(n-1,m)$$
那麼我們考慮一個可能合法方案的不合法情況
該情況兩條路徑必定有交點,考慮對最後一個交點的兩端路徑進行翻轉,那麼$$(1,2)->(n-1,m),(2,1)->(n,m-1)$$可以翻轉為$$(1,2)->(n,m-1),(2,1)->(n-1,m)$$
而且翻轉後與翻轉前的方案一一對應
所以答案是$$solve(1,2,n-1,m)\times solve(2,1,n,m-1) - solve(1,2,n,m-1)\times solve(2,1,n-1,m)$$
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int mod = 1e9 + 7;
template<typename _T>
inline void read(_T &x)
{
x = 0;int f= 1;char s = getchar();
while(s<'0'||s>'9'){f=1;if(s=='-')f=-1;s=getchar();}
while('0'<=s&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
x*=f;
}
const int np = 3e3+ 5;
char s[np][np];
int a[np][np];
int f[np][np];
int n,m;
inline int solve(int st_x,int st_y,int end_x,int end_y)
{
f[st_x][st_y] = 1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(a[i][j])
{
f[i][j] = f[i-1][j] + f[i][j-1] + f[i][j];
f[i][j]%=mod;
}
else f[i][j] = 0;
}
}
return f[end_x][end_y];
}
signed main()
{
read(n);
read(m);
for(int i=1;i<=n;i++)
{
scanf("%s",s[i] + 1);
int len = strlen(s[i] + 1);
for(int j=1;j<=len;j++)
a[i][j] = s[i][j]=='.'?1:0;
}
int x = solve(1,2,n-1,m);
memset(f,0,sizeof(f));
int y = solve(2,1,n,m-1);
memset(f,0,sizeof(f));
int xx = solve(1,2,n,m-1);
memset(f,0,sizeof(f));
int yy = solve(2,1,n-1,m);
cout<< ((x * y - xx * yy + mod)%mod + mod) %mod;
}
有思維難度的容斥 dp
我們先展示兩種科技:子集反演、二項式反演
子集反演:
二項式反演(基本:
\[f(n) = \sum_{i=0}^n(-1)^i\dbinom{n}{i}g(i)\]\[g(n) = \sum_{i=0}^n(-1)^i\dbinom{n}{i}f(i) \]擴充套件:
\[f(n) = \sum_{i=0}^n\dbinom{n}{i}g(i) \]\[g(n) = \sum_{i=0}^n(-1)^{n-i}\dbinom{n}{i}f(i) \]二項式反演有廣義容斥證明方法,這裡不寫了
子集反演本質就是廣義容斥,很好想,不證了
P3349 [ZJOI2016]小星星
看到 N 的範圍很小,直接dp
看一個樸素的狀壓 dp 解法
dp[i][j][S] 表示以 i 為根的子樹選 j 標號,它的子樹選了 S 這個集合的標號
每次轉移列舉子集即可,總複雜度 \(O(n^33^n)\)
顯然過不去。
我們考慮暴力中求的是唯一對應,即 i 的子樹唯一對應集合 S
優化掉列舉子集的複雜度,需要使用容斥原理,
將 dp 方程改為以 i 為根的子樹選 j 標號,它的子樹至多在 S 這個集合內選標號。
\[dp[u][i][S] = \prod_{v\in son(u)}\sum_{i,j\in S}dp[v][j][S] \]每次列舉一個 S,然後在樹上 dp 即可
統計答案的時候用一下前面提到的『子集反演』科技,逆向求 g
雖然提倡不要學科技,但是題解中不用科技硬找容斥係數也太奇怪了……
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define lowbit(x) (x&(-x))
const int mod = 1e9 + 7;
template<typename _T>
inline void read(_T &x)
{
x = 0;int f= 1;char s = getchar();
while(s<'0'||s>'9'){f=1;if(s=='-')f=-1;s=getchar();}
while('0'<=s&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
x*=f;
}
const int np = 19;
const int npp = (1ll << 21) + 5;
int n,m;
int head[np] , ver[np * 4] , nxt[np * 4];
int tit;
inline void add(int x,int y)
{
ver[++tit] = y;
nxt[tit] = head[x];
head[x] = tit;
}
int Edge[np][np];
int f[np][np];
int g[npp];
inline void dfs(int x , int fa,int S)
{
for(int i=1;i<=n;i++) f[x][i] = 1ll;
for(int i=head[x] , v;i;i=nxt[i])
{
v = ver[i];
if(v == fa) continue;
dfs(v , x,S);
for(int i=1;i<=n;i++)
{
if(!(1ll<<(i-1) & S)) continue;
int op = 0;
for(int j=1;j<=n;j++)
{
if(i == j) continue;
if(!(1ll<<(j-1) & S)) continue;
if(Edge[i][j]) op += f[v][j];
}
f[x][i] *= op ;
}
}
}
signed main()
{
read(n);
read(m);
for(int i=1,u,v;i<=m;i++)
{
read(u);
read(v);
Edge[u][v] = 1;
Edge[v][u] = 1;
}
for(int i=1,u,v;i<=n-1;i++)
{
read(u);
read(v);
add(u ,v);
add(v ,u);
}
int f_ = 0;
for(int i = 0; i < 1ll<<n ; i++)
{
dfs(1 , 0 , i);
for(int j=1;j<=n;j++)
g[i] += f[1][j];
int cnt_x = 0 ;
for(int x = i;x;x-=lowbit(x)) cnt_x++;
f_ += (n - cnt_x) & 1?-g[i]:g[i];
}
cout<<f_;
}
# P2167 [SDOI2009]Bill的挑戰
有二項式反演做法……
但我不會,直接暴力狀壓 dp
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define lowbit(x) (x & (-x))
const int mod = 1000003;
template<typename _T>
inline void read(_T &x)
{
x = 0;int f= 1;char s = getchar();
while(s<'0'||s>'9'){f=1;if(s=='-')f=-1;s=getchar();}
while('0'<=s&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
x*=f;
}
const int np = 17;
const int npp = (1 << 17) + 5;
int f[np * 4][npp];
char c[np][np << 2];
int v[np * 4][29];
inline int Gets(char x)
{
return x - 'a' + 1;
}
int st[2333];
int top;
inline void print(int x)
{
while(x) st[++top] = x&1 , x>>=1;
for(int i=1;i<=top;i++)
{
std::cout<<st[i];
}
std::cout<<'\n';
}
signed main()
{
int T;
read(T);
while(T--)
{
memset(f,0,sizeof(f));
int n,k;
read(n);
read(k);
int len;
for(int i=1;i<=n;i++)
{
scanf("%s",c[i] + 1);
len = strlen(c[i] + 1);
}
for(int i=1;i<=len;i++)
{
for(int j=1;j<=26;j++)
{
int op = 0;
for(int c_=1;c_<=n;c_++)
{
if(Gets(c[c_][i]) == j || c[c_][i] == '?')
{
op += 1 << c_ - 1;
}
}
v[i][j] = op;
}
}
f[0][(1<<n) - 1] = 1;
for(int i=1;i <= len;i++)
{
for(int ch=1;ch<=26;ch++)
{
for(int T = 0 ; T < 1<<n;T++)
{
f[i][T & v[i][ch]] += f[i - 1][T];
f[i][T & v[i][ch]] %= mod;
}
}
}
int Ans =0 ;
for(int i=0;i < 1<<n ; i++)
{
int cnt_ = 0;
for(int u = i;u ; u -= lowbit(u)) cnt_++;
if(cnt_ == k) Ans += f[len][i] , Ans %= mod;
}
cout<<Ans<<'\n';
}
}