组合数学:从入门到入土

更新ing

菜鸡 \(wljss\) 来讲组合数学啦。

组合数学博大精深,主要是爱数数的人上大学了,从模拟赛到NOI都出现过。

一些技巧可以看这里

其实看完上边那个也就没啥好说的了

上例题吧

P1595 信封问题

这里有讲解,直接放代码233

#include<iostream>
using namespace std;
long long ans[21];
int main() 
{
    int n;
    cin >> n;
    ans[2] = 1; ans[3] = 2;
    for (int i = 4; i <= 20; ++i)
        ans[i] = (i - 1) * (ans[i - 1] + ans[i - 2]);
    cout << ans[n];
    return 0;
}

P3197 [HNOI2008]越狱

首先我们可以发现总的方案很好求,就是 \(m^n\).

所以我们采用求 补集 的思想

有多少可能不会发生越狱呢?

第一个人有 \(m\) 种可能的信仰。

从第二个人开始,每个人的信仰都要和上一个人不同,每个人有 \(m-1\) 种。

所以答案就是 \(m^n - m \times (m-1)^{n-1}\)

#include<iostream>
#include<cstdio>
#define LL long long
using namespace std;
LL n, m;
const LL mod = 100003;
LL ksm(LL a, LL b, LL mod) 
{
    LL ans = 1;
    for (; b; b >>= 1, a = a * a % mod) {if (b & 1)ans = ans * a % mod;}
    return ans;
}
int main() 
{
    cin >> m >> n;
    cout << (ksm(m, n, mod) + mod - m * ksm(m - 1, n - 1, mod) % mod) % mod;
    return 0;
}

P1057 传球游戏

\(dp[i][j]\) 为第 \(i\) 个人在第 \(j\) 轮拿到球的方案数。

每次考虑一个人的求可能从哪个方向上传过来.

\(f[i][j]=f[i-1][j-1]+f[i+1][j-1]\)

当然 \(1\) 号和 \(n\) 号要特殊处理一下。

#include<iostream>
using namespace std;
int f[31][31];
int main() 
{
    int n, m;
    cin >> n >> m;
    f[1][0] = 1;
    for (int i = 1; i <= m; ++i) 
    {
        f[1][i] = f[2][i - 1] + f[n][i - 1];
        for (int j = 2; j <= n - 1; ++j) 
            f[j][i] = f[j - 1][i - 1] + f[j + 1][i - 1];
        f[n][i] = f[n - 1][i - 1] + f[1][i - 1];
    }
    cout << f[1][m];
    return 0;
}

P6057 [加油武汉]七步洗手法

总的三元环个数很好求,任意选出 \(3\) 个点就能组成,所以总的三元环个数是 \(C_n^3\)

我们采用求 补集 的思想,不同色的三元环有多少个?

考虑每个点,如果有 \(d\) 条白边,那么就有 \(n-d-1\) 条黑边,两两组合就会产生 \(d \times (n-d-1)\) 个三元环

考虑这样我们求出来的是啥?考虑对于每个不同色的三元环,会被每对不同色的边各枚举一次,一共会被枚举 \(2\) 次.

所以\(/2\)后才是真正的不同色三元环个数。

#include<iostream>
#include<cstdio>
using namespace std;
int n, m;
long long tmp;
const int N = 100010;
int du[N];
int main() 
{
    cin >> n >> m;
    for (int i = 1, x, y; i <= m; ++i)
        scanf("%d%d", &x, &y), ++du[x], ++du[y];
    for (int i = 1; i <= n; ++i)tmp += (long long)du[i] * (n - du[i] - 1);
    cout << (long long)n*(n - 1)*(n - 2) / 6 - tmp / 2;
    return 0;
}

P1535 [USACO08MAR]Cow Travelling S

简单 \(DP\),设 \(dp[i][j][k]\) 为走了 \(k\) 步,走到坐标 \((i,j)\) 的方案数。

转移的话直接从周围 \(4\) 个方向转移就行了。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int n, m, t, x1, y1, x2, y2;
char mp[105][105];
int l[105][105];
long long dp[105][105][20];
int fx[10], fy[10];
int main() 
{
    cin >> n >> m >> t;
    for (int i = 1; i <= n; ++i)scanf("%s", mp[i] + 1);
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            if (mp[i][j] == '*')l[i][j] = 1;
    cin >> x1 >> y1 >> x2 >> y2;
    fx[1] = 0; fx[2] = 0; fx[3] = 1; fx[4] = -1;
    fy[1] = 1; fy[2] = -1; fy[3] = 0; fy[4] = 0;
    dp[x1][y1][0] = 1;
    for (int k = 1; k <= t; ++k)
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j)
                for (int d = 1; d <= 4; ++d)
                    if (!l[i + fx[d]][j + fy[d]])
                        dp[i][j][k] += dp[i + fx[d]][j + fy[d]][k - 1];
    printf("%lld", dp[x2][y2][t]);
    fclose(stdin); fclose(stdout);
    return 0;
}

P1144 最短路计数

普通求最短路的话一定要用DIJ,SPFA早就死了。

\(f[i]\)当前情况下 \(i\) 的最短路计数

首先初始化 \(f[1]=1\)

然后考虑求最短路的过程.

如果 \(dis[to[i]] > dis[x] + 1\),由于最短路更新了, 当前情况最短路只能由 \(x\) 走过来,\(f[to[i]] = f[x]\)

如果 \(dis[to[i]] = dis[x] + 1\),最短路没有更新, 当前情况最短路也能由 \(x\) 走过来,\(f[to[i]] += f[x]\)

跑最短路的时候更新就行了.

#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#define pr pair<int,int>
using namespace std;
int n, m, tot;
const int N = 1000010, M = 2000010, mod = 100003;
int head[N], to[M << 1], nt[M << 1], dis[N], f[N], vis[N];
priority_queue<pr>q;
void add(int f, int t) 
{
    to[++tot] = t; nt[tot] = head[f]; head[f] = tot;
}
int main() 
{
    cin >> n >> m;
    for (int i = 1, x, y; i <= m; ++i)
        scanf("%d%d", &x, &y), add(x, y), add(y, x);
    memset(dis, 0x3f, sizeof(dis));
    dis[1] = 0; f[1] = 1; q.push(pr(0, 1));
    while (!q.empty()) 
    {
        int x = q.top().second; q.pop();
        if (vis[x])continue; vis[x] = 1;
        for (int i = head[x]; i; i = nt[i])
            if (dis[to[i]] == dis[x] + 1) {(f[to[i]] += f[x]) %= mod;}
            else if (dis[to[i]] > dis[x] + 1) 
            {
                dis[to[i]] = dis[x] + 1; f[to[i]] = f[x];
                q.push(pr(-dis[to[i]], to[i]));
            }
    }
    for (int i = 1; i <= n; ++i)printf("%d\n", f[i]);
    return 0;
}

P1450 [HAOI2008]硬币购物

首先每次背包一下答案是对的,但是复杂度太高。

对于这种有选取数量限制的计数题,我们通常枚举有哪些突破了数量限制,然后容斥。

对于这道题来说,我们先用四种面值做一个完全背包。

然后枚举哪些面值肯定会突破限制,这个可以通过 \(tmp -= c * (b + 1)\) 来实现,也就是先选出来 \(b+1\) 个,再怎么选都会突破限制。

答案并不是 有0个强制突破限制的情况,因为虽然没有强制突破的情况,但因为随便选还是有突破的情况。

所以我们需要减去强制有1个突破的情况。

然后会发现减的有点多,要加上强制有2个突破的情况.以此类推。

#include<iostream>
#include<cstdio>
using namespace std;
int n, s, opt;
long long ans, tmp;
int b[5], c[5];
long long f[100010];
int main() 
{
    cin >> c[1] >> c[2] >> c[3] >> c[4] >> n;
    f[0] = 1;
    for (int i = 1; i <= 4; ++i)
        for (int j = c[i]; j <= 100000; ++j)f[j] += f[j - c[i]];
    while (n--) 
    {
        scanf("%d%d%d%d%d", &b[1], &b[2], &b[3], &b[4], &s);
        ans = 0;
        for (int i = 0; i < (1 << 4) - 1; ++i) 
        {
            tmp = s; opt = 1;
            for (int j = 1; j <= 4; ++j)
                if ((i >> (j - 1)) & 1)tmp -= 1ll * c[j] * (b[j] + 1), opt = -opt;
            if (tmp < 0)continue;
            ans += opt * f[tmp];
        }
        cout << ans << '\n';
    }
    return 0;
}

P4071 [SDOI2016]排列计数

首先枚举哪些位置满足 \(a_i=i\),那么剩下的 \(n-m\) 个数就需要错排。

所以答案就是 \(C_n^m \times f[n-m]\)

#include<iostream>
#include<cstdio>
#define LL long long
using namespace std;
int T, n, m;
const int N = 1000010, M = 1000000, mod = 1e9 + 7;
LL jc[N], inv[N], f[N];
LL ksm(LL a, LL b, LL mod) 
{
    LL res = 1;
    for (; b; b >>= 1, a = a * a % mod)
        if (b & 1)res = res * a % mod;
    return res;
}
void YYCH() 
{
    jc[0] = jc[1] = inv[0] = inv[1] = 1;
    for (int i = 2; i <= M; ++i)jc[i] = jc[i - 1] * i % mod;
    inv[M] = ksm(jc[M], mod - 2, mod);
    for (int i = M - 1; i >= 1; --i)inv[i] = inv[i + 1] * (i + 1) % mod;

    f[0] = 1; f[1] = 0; f[2] = 1; f[3] = 2;
    for (int i = 4; i <= M; ++i)f[i] = (i - 1) * (f[i - 1] + f[i - 2]) % mod;
}
LL C(int n, int m) {return jc[n] * inv[m] % mod * inv[n - m] % mod;}
int main() 
{
    YYCH();
    cin >> T;
    while (T--) 
    {
        scanf("%d%d", &n, &m);
        printf("%lld\n", C(n, m)*f[n - m] % mod);
    }
    return 0;
}

P2513 [HAOI2009]逆序对数列

\(f[i][j]\) 为用 \(1\) ~ \(i\) 组成的序列有j个逆序对的方案数。

我们将每个数一个一个插入原序列,考虑第 \(i\) 个数放在原来的序列的哪个位置,由于之前的数都比 \(i\) 小,所以如果插在 \(k\) 个数后面,就会增加 \(k\) 个逆序对。

所以 \(\displaystyle f[i][j]=\sum_{k=0}^{min(i-1,j)}f[i-1][j-k]\)

发现后面那一段是连续的一段,可以用前缀和优化一下。

#include<iostream>
#include<cstdio>
using namespace std;
int n, k, tot = 0, p = 10000;
int ans[1001][1001];
int main() 
{
    cin >> n >> k;
    ans[1][0] = 1;
    for (int i = 2; i <= n; ++i) 
    {
        tot = 0;
        for (int j = 0; j <= k; ++j) 
        {
            tot += ans[i - 1][j];
            ans[i][j] = tot % p;
            if (j >= i - 1) 
            {
                tot = tot - ans[i - 1][j - i + 1];
                tot = (tot + p) % p;
            }
        }
    }
    cout << ans[n][k];
    return 0;
}

P5664 Emiya 家今天的饭

考虑容斥:合法方案:总方案-不合法方案

总方案很好求,可以 \(DP\) ,也可以用 \(\displaystyle (\prod_{i=1}^{n} (1 + \sum_{j=1}^{m}a[i][j]))-1\) 来求。

如果一个方案不合法,一定是某个食材出现次数超过了 \(\lfloor \frac{k}{2} \rfloor\) 次,并且能造成这个的最多只有 \(1\) 个食材。

枚举哪个食材不合法,\(DP\)\(g[i][j][k]\) 为前 \(i\) 个里面,一共选了 \(j\) 个,其中 \(id\) 选了 \(k\) 个的方案数

时间复杂度 \(O(m n^3)\) ,只有84分

#include<iostream>
#include<cstdio>
#define LL long long
using namespace std;
int n, m, ans;
const int N = 105, M = 2005, mod = 998244353;
int a[N][M];
LL f[N][N], g[N][N][N];
void solve1() 
{
    int tmp;
    f[0][0] = 1;
    for (int i = 1; i <= n; ++i) 
    {
        f[i][0] = 1; tmp = 0;
        for (int j = 1; j <= m; ++j)(tmp += a[i][j]) %= mod;
        for (int j = 1; j <= i; ++j)f[i][j] = (f[i - 1][j] + f[i - 1][j - 1] * tmp) % mod;
    }
}
void solve2(int id) 
{
    int tmp;
    g[0][0][0] = 1;
    for (int i = 1; i <= n; ++i) 
    {
        g[i][0][0] = 1; tmp = 0;
        for (int j = 1; j <= m; ++j)
            if (j != id)(tmp += a[i][j]) %= mod;
        for (int j = 1; j <= i; ++j) 
        {
            for (int k = 0; k <= j; ++k)g[i][j][k] = g[i - 1][j][k];
            for (int k = 0; k <= j; ++k)(g[i][j][k] += g[i - 1][j - 1][k] * tmp) %= mod;
            for (int k = 1; k <= j; ++k)(g[i][j][k] += g[i - 1][j - 1][k - 1] * a[i][id]) %= mod;
        }
    }
}
int main() 
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)scanf("%d", &a[i][j]);
    solve1();
    for (int i = 1; i <= m; ++i) 
    {
        solve2(i);
        for (int j = 1; j <= n; ++j)
            for (int k = j / 2 + 1; k <= j; ++k)
                (f[n][j] -= g[n][j][k]) %= mod;
    }
    for (int i = 1; i <= n; ++i)(ans += f[n][i]) %= mod;
    cout << (ans % mod + mod) % mod;
    return 0;
}

发现同时记录 \(j\)\(k\) 就有点多余,重新设 \(g[i][j]\) 为前 \(i\) 个里,\(id\) 选的比其他的多 \(j\) 个时的方案数。

时间复杂度就成了 \(O(mn^2)\),可以过,但是因为可能 \(j\) 有可能是负数,所以需要整体下标加上 \(n\)

#include<iostream>
#include<cstring>
#include<cstdio>
#define LL long long
using namespace std;
int n, m, ans;
const int N = 105, M = 2005, mod = 998244353;
int a[N][M];
LL f[N][N], s[N], g[N][N << 1];
void solve1() 
{
    ans = 1;
    for (int i = 1; i <= n; ++i) 
    {
        for (int j = 1; j <= m; ++j)(s[i] += a[i][j]) %= mod;
        ans = (LL)ans * (s[i] + 1) % mod;
    }
    --ans;
}
void solve2(int id) 
{
    int tmp;
    g[0][n] = 1;
    for (int i = 1; i <= n; ++i) 
    {
        tmp = (s[i] - a[i][id]) % mod;
        for (int j = 0; j <= 2 * n; ++j)g[i][j] = g[i - 1][j];
        for (int j = 0; j <= 2 * n - 1; ++j)(g[i][j] += g[i - 1][j + 1] * tmp) %= mod;
        for (int j = 1; j <= 2 * n; ++j)(g[i][j] += g[i - 1][j - 1] * a[i][id]) %= mod;
    }
}
int main() 
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)scanf("%d", &a[i][j]);
    solve1();
    for (int i = 1; i <= m; ++i) 
    {
        solve2(i);
        for (int j = 1; j <= n; ++j)(ans -= g[n][n + j]) %= mod;
    }
    cout << (ans % mod + mod) % mod;
    return 0;
}
posted @ 2020-06-16 09:30  wljss  阅读(565)  评论(0编辑  收藏  举报