【算法竞赛入门经典—训练指南】学习笔记(含例题代码与思路)第二章:数学基础...

第二章难度要稍微高一点,有很多以前没怎么见过的东西,所以会尽量详细地记录。

  • 计数原理:加法原理,乘法原理,容斥原理

    • 容斥原理:即选区去重的思想。

    • 通常实现方法是枚举子集,复杂度\(2^n\)

  • 排列数:\(P_n^k = \frac{n !}{(n - k)!}\),其中\(P(n, k)\)代表\(n\)个不同的数选出\(k\)个排成一排的方案数。

  • 组合数:\(C_n^m = \frac{n!} {m! (n - m)!}\),其中\(C(n, m)\)代表\(n\)个不同的数选出\(m\)个的方案数

  • 二项式定理:\((a + b)^n = \sum_{i=1}^n{C_n^ka^{n-k}b^k}\)

    • 似乎还有相关的更厉害的东西,回头学完会回来补充。
  • 有重复元素的全排列:有\(k\)个元素,其中第\(i\)个有\(A_i\)个,问选择方案。

    • 做完全排列之后会出现相同元素组成的一个子序列被认为不同的情况。对每个这样的子序列除去其可以形成的排列方法即可。即\(\frac{n!}{\prod A_i!}\)种情况。
  • 可以重复选择的组合:组合中每一个数可以选任意次。

    • 问题转化为求\(x_1+x_2+x_3+...+x_n=m\)的非负整数解的个数

    • \(y_i = x_i + 1\),原方程转为\(y_1 + y_2 + ... + y_n = m + n\)

    • 因为每个\(y\)至少是\(1\),所以可以想象成给球添加隔板,即在\(m + n - 1\)个分割线中选择\(n - 1\)个,把它们分割成\(y_1,y_2..y_n\)\(n\)部分。答案就是\(C_{n+m-1}^{n-1}\)


例题\(1\) 象棋中的皇后(\(UVa11538\)

  • 注意棋子只有两个,放置方法有同行同列同对角三种,三种情况两两不想交。所以我们直接对每行每列每个对角选两个棋子相加,也就是求一次组合数就可以了。

  • 诶诶诶听说你们这还有一个平方和公式?抱歉我不会推\(=w=\)。暴力就完事了。

  • (当然你如果非要推的话,上个等差数列求和就可以啦~)

#include 
using namespace std;

#define int long long

int A (int n, int m) {return n * m * (m - 1);}
int B (int n, int m) {return m * n * (n - 1);}
int D (int n, int m) {
    int ret = (m - n + 1) * n * (n - 1);
    for (int i = 1; i <= n - 1; ++i) ret += 2 * i * (i - 1);
    return ret;
}

int n, m;

signed main () {
    while (cin >> n >> m && n && m) {
        if (n > m) swap (n, m);
        if (n == 0 || m == 0) {puts ("0"); continue;}
        int ans = A (n, m) + B (n, m) + 2 * D (n, m);
        cout << ans << endl;
    }
}

例题\(2\) 数三角形(\(UVa11401\)

  • 写法:设最大边长为\(x\)的三角形有\(c(x)\)个,答案为\(f(x)\)。分类讨论,\(O(N)\)真·数数。

  • 思想:摁住容易控制的特殊枚举项,再去枚举其他的会容易枚举。

#include 
using namespace std;

#define int long long
const int N = 1000010;

int n, f[N];

signed main () {
//  freopen ("data.in", "r", stdin);
    for (int x = 4; x <= 1000000; ++x) {
        f[x] = f[x - 1] + ((x - 1) * (x - 2) / 2 - (x - 1 - x / 2)) / 2;
    }
    while (cin >> n && n >= 3) {
        cout << f[n] << endl;
    }
}

例题\(3\) 拉拉队(\(UVa11806\)

  • 如果没有限制直接上组合数就好了,关键在于处理第一行,最后一行,第一列,最后一列必须有石子的限制。

  • 我们可以先假定先固定给某些个【第一\(/\)最后】一【行\(/\)列】放上一个子,然后减去重复的部分就可以了。

  • 简单地说:手动容斥即可。

#include  
using namespace std;

#define int long long
const int N = 500 + 5;
const int Mod = 1000007;

int add (int x, int y) {return (((x + y) % Mod) + Mod) % Mod;}
int mul (int x, int y) {return (((x * y) % Mod) + Mod) % Mod;}

int T, m, n, k, kase, C[N][N];

signed main () {
//  freopen ("data.in", "r", stdin);
//  freopen ("data.out", "w", stdout);
    C[0][0] = 1;
    for (int i = 1; i < N; ++i) {
        for (int j = 1; j <= i; ++j) {
            C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % Mod;
        }
        C[i][0] = 1;
    }
    cin >> T;
    while (T--) {
        int ans = 0;
        cin >> m >> n >> k; 
        ans = C[n * m][k];
        ans = add (ans, -2 * C[(m - 1) * n][k] - 2 * C[m * (n - 1)][k]);
        ans = add (ans, C[(m - 2) * n][k] + C[m * (n - 2)][k] + 4 * C[(m - 1) * (n - 1)][k]);
        ans = add (ans, -2 * C[(m - 2) * (n - 1)][k] - 2 * C[(m - 1) * (n - 2)][k]);
        ans = add (ans, C[(m - 2) * (n - 2)][k]);
        cout << "Case " << ++kase << ": " << ans << endl;
    }
}

  • 递推关系

    • 兔子繁殖:\(f(x) = f(x - 1) + f(x - 2)\) 。斐波那契数列的经典应用。

    • 凸多边形的三角剖分数:\(f(x) = \sum_{i=2}^{n-1} {f(i)*f(n +1-i)}\),称为\(Catalan\)数列。(目前为止还没怎么了解过这个东西。。我数学果然太差了。。。)

    • \(n\)根火柴能组成的数的个数(不含前导\(0\)):建立一个状态图,编号相隔等于某个数字的火柴数的两个点可以建立转移。注意判断前导\(0\)

    • 立方数之和:问一个数\(n\)能被分成多少种立方数的和的形式。写法:分层图。(不需要显示建图)

    • 村民排队:树上\(DP\),设\(f(x)\)为以\(x\)为根的子树内顺序的排列方式。推一波式子可以得到\(f(u) = (sz(u) - 1)!\cdot \frac{\prod f(v_i)}{\prod sz(v_i)!}\)。这个式子复杂度已经很好了,但没想到居然还能优化=_=。仔细思考会发现每个非根节点\(u\)会以\(sz(u)-1!\)的形式出现在分子一次,以\(sz(u)!\)的形式出现在分母一次,约分一下相当于分子为\(1\),分母为\(sz(u)\)。也就是说我们可以直接得到\(f(root) = \frac{sz(root - 1)!}{\prod_{i=1}^n sz(i)}\)

    • 带标号连通图计数:考虑反转限制,数一下带标号不连通图的个数。(因为想连通需要一直连下去,不连通只需要断一次嘛~)详情请见蓝书。


例题\(4\) 多叉树便历(\(LA3516\)

  • 划分原序列的每一个子区间,从短向长进行递推。转移条件是首尾相接。
#include 
using namespace std;

const int N = 300 + 5;
const int Mod = 1e9;

int n, dp[N][N]; char s[N];

int dfs (int l, int r) { 
    if (l == r) return 1;
    if (s[l] != s[r]) return 0;
    if (dp[l][r] >= 0) return dp[l][r];
    int ret = 0;
    for (int k = l + 2; k <= r; ++k) {
        if (s[l] == s[k]) {
            ret = (0ll + ret + 1ll * dfs (l + 1, k - 1) * dfs (k, r)) % Mod;
        }
    }
    return dp[l][r] = ret;
}

int main () {
//  freopen ("data.in", "r", stdin);
    while (cin >> (s + 1)) {
        n = strlen (s + 1);
        memset (dp, -1, sizeof (dp));
        cout << dfs (1, n) << endl;
    }
}

例题\(5\) 数字和与倍数(\(UVa11361\)

  • 数位\(DP\)。注意到要膜各个数位之和同余之后就可以把\(k\)的范围缩小为\(100\)以内了。
#include 
using namespace std;

#define int long long
const int N = 50 + 5;
const int M = 100 + 5;

int T, l, r, k, arr[N], bin[N];

int dp[2][N][M][M];
bool vis[2][N][M][M];

int add (int x, int y) {x %= k, y %= k; return (((x + y) % k) + k) % k;}

int dfs (int wei, bool lim, int sum1, int sum2) {
//  printf ("wei = %d, lim = %d, sum1 = %d, sum2 = %d, vis = %d\n", wei, lim, sum1, sum2, vis[lim][wei][sum1][sum2]);
    if (wei == 0) {
        return sum1 == 0 && sum2 == 0;
    }
    if (vis[lim][wei][sum1][sum2]) {
        return dp[lim][wei][sum1][sum2];
    }
    vis[lim][wei][sum1][sum2] = true;
    int m = lim ? arr[wei] : 9, ans = 0;
    for (int i = 0; i <= m; ++i) {
//      printf ("i * bin[wei - 1] = %d\n", i * bin[wei - 1]);
        int _sum1 = add (sum1, -i * bin[wei - 1]);
        int _sum2 = add (sum2, -i);
//      printf ("i = %d, _sum1 = %d, _sum2 = %d\n", i, _sum1, _sum2);
//      cout << "use = " << i << endl;
        ans += dfs (wei - 1, lim & (i == arr[wei]), _sum1, _sum2);
    }
//  printf ("k = %d, dp[%d][%d][%d][%d] = %d\n", k, lim, wei, sum1, sum2, ans);
    return dp[lim][wei][sum1][sum2] = ans;
//  return ans;
}

int get_ans (int x) {
    int wei = 0;
    while (x != 0) {
        arr[++wei] = x % 10;
        x /= 10;
    }
//  memset (dp, 0, sizeof (dp));
    memset (vis, 0, sizeof (vis));
//  for (int i = wei; i >= 1; --i) printf ("%d ", arr[i]); printf ("\n");
    return dfs (wei, true, 0, 0);
}
signed main () {
    cin >> T; bin[0] = 1;
    for (int i = 1; i <= 18; ++i) bin[i] = bin[i - 1] * 10;
    while (T--) {
        cin >> l >> r >> k;
        if (k > 85) {puts ("0"); continue;}
        cout << get_ans (r) - get_ans (l - 1) << endl;
    }   
}

例题\(6\) 葛伦堡博物馆(\(LA4123\)

  • 如果要看到每一个位置,显然不能有凹进去的部分。(不然一定会有看不到的部分。)

  • 也就是说:一定可以在最上最下最左最右找到四条\(RR\)边,它们之间以不凹陷的梯形序列连接,只要你把边做的够长就不会有看不到的角落。可以发现所有可行解都可以化为这样的形式。

  • 接下来一步跨度比较大:原问题因此可以直接转化为\(\frac{n+4}{2}\)\(R\)\(\frac{n-4}{2}\)\(O\)形成一个环形序列,不存在两个相邻\(O\)(含首位相接)的序列个数。递推一发即可。

  • 第三步的式子还可以优化为\(O(N)\)的:只要想到\(R\)的个数减去\(O\)的个数不会超过\(5\)即可。(每两个对\(R-O\)有贡献的\(R\)至少要对应一个\(O\)

  • 注意\(n<4\)或者\(n\)为奇数时要特判。

#include 
using namespace std;

const int N = 1010;
typedef long long LL;

int n, kase; LL dp[N][N][2][2];

int main () {
//  freopen ("data.in", "r", stdin);
    //dp[i][j][k][l] -> i * R, j * O, now is R = 0 / O = 1, begin is R = 0 / O = 1;
    dp[1][0][0][0] = dp[0][1][1][1] = 1;
    for (int tot = 2; tot <= 1000; ++tot) {
        for (int r = tot / 2; r <= tot / 2 + 3; ++r) {
            int o = tot - r;
            if (r) dp[r][o][0][0] = dp[r - 1][o][0][0] + dp[r - 1][o][1][0];
            if (r) dp[r][o][0][1] = dp[r - 1][o][0][1] + dp[r - 1][o][1][1];
            if (o) dp[r][o][1][0] = dp[r][o - 1][0][0];
            if (o) dp[r][o][1][1] = dp[r][o - 1][0][1];
        }
    }
    while (cin >> n && n) {
        if (n & 1 || n <= 3) {
            cout << "Case " << ++kase << ": " << 0 << endl;     
        } else {     
            int R = (n + 4) / 2, O = (n - 4) / 2;
            cout << "Case " << ++kase << ": " << dp[R][O][0][0] + dp[R][O][0][1] + dp[R][O][1][0] << endl;
        }
    } 
}

例题\(7\) 串并联网络(\(UVa10253\)

  • 不需要那么清楚的去理串并联的概念,只需要把它们分为树上的两类连边就可以了。

  • 串联下面要接并联,并联下面要接串联。(不然就可以合并了。)问题就变成了“共\(n\)个叶子,每个非叶节点至少有两个子节点”的树的个数。

  • 对每一个节点的所有子树我们可以做一个整数划分,然后求一下对每个子树有多少种可选姿态,做一下可重组合。如果不做整数划分而是直接放进\(DP\)数组里递推还会更快。

  • 题目真的很值得一做。回头再来复习一遍吧~

include 
using namespace std;

#define int long long

int C (int n, int m) {
    double ans = 1;
    for (int i = n - m + 1; i <= n; ++i) ans *= i;
    for (int i = 1; i <= m; ++i) ans /= i;
    return (long long)(ans + 0.5);
}

const int N = 30 + 5;
int n, f[N], d[N][N]; 

signed main () {
//  freopen ("data.in", "r", stdin);
    f[1] = 1; for (int i = 0; i <= 30; ++i) d[i][0] = 1;
    for (int i = 1; i <= 30; ++i) d[i][1] = 1, d[0][i] = 0;
    
    for (int i = 1; i <= 30; ++i) {
        for (int j = 2; j <= 30; ++j) {
            for (int p = 0; p * i <= j; ++p) {
                d[i][j] += C (f[i] + p - 1, p) * d[i - 1][j - p * i]; 
            }
        }
        f[i + 1] = d[i][i + 1];
    }
    
    while (cin >> n && n) {
        cout << (n == 1 ? 1 : 2 * f[n]) << endl; 
    }
}

  • 线性筛:略

  • \(GCD\)\(ExGCD\):略。

    • 需要注意的点:\(ExGCD\)求出来的是\(|x|+|y|\)最小的解。
  • 费马小定理:\(AX≡1(Mod\) \(p)\) 其中\(p\)为质数时,\(X\)有解为\(x^{p - 2}\)

  • 欧拉函数(欧拉欧拉欧拉):线性筛直接求就可以了。

    • 积性函数。

    • 组合意义上是小于\(N\)的和\(N\)互质的数的个数。

  • 逆元:通常用\(ExGcd\)或线性阶乘求。


例题\(8\) 总是整数

  • 最麻烦的地方居然是字符串的处理\(2333333\)

  • 获取多项式之后可以随机带入一些值看是不是整数。效果贼好。

  • 正解思路:

    • 次数为\(1\)的多项式:看公差和第一项是否为分母倍数。

    • 次数为\(2\)的多项式:看两项差和第一项是否为分母倍数,其中两项差为一个次数为\(1\)的多项式,验证这个多项式是否为分母倍数等价于验证\(f(1), f(2), f(3)\)

    • 以此类推。只需验证代入\([1, k]\)是否均为整数。

#include 
using namespace std;

#define int long long
const int N = 1000010;

int n, tot, divv, kase, coef[N], ind[N]; string str;

bool is_poly (int x) {
    return isalpha (str[x]) || isdigit (str[x]) || str[x] == '^';
} 

void process (int l, int r) {
    tot = tot + 1;
//  cout << "l = " << l << " r = " << r << endl;
    int numl = l, numr = l - 1;
    coef[tot] = ind[tot] = 0;
    while (isdigit (str[numr + 1])) {
        coef[tot] = coef[tot] * 10 + str[++numr] - '0';
    }
    coef[tot] = numl <= numr ? coef[tot] : 1;
    if (str[l - 1] == '-') coef[tot] *= -1;
//  cout << "coef[" << tot << "] = " << coef[tot] << endl;
    if (numr == r) return; //常数
    if (numr + 1 == r) {ind[tot] = 1; return;} //一次项
    int powl = numr + 2;
    while (isdigit (str[powl + 1])) {
        ind[tot] = ind[tot] * 10 + str[++powl] - '0';
    } 
}

void get_poly () {
    int fin, posl, posr; divv = 0;
    for (int i = 0; i < n; i = posr + 1) {
        if (str[i] == ')') {fin = i + 2; break;}
        posl = i; while (!is_poly (posl)) posl++; 
        posr = posl; while (is_poly (posr)) posr++;
        posr--; process (posl, posr);
//      cout << "coef = " << coef[tot] << " ind = " << ind[tot] << endl;
    }
    for (int i = fin; i < n; ++i) {
        divv = divv * 10 + str[i] - '0';
    }
//  cout << "div = " << divv << endl;
}

int fpow (int x, int y) {
    int res = 1; 
    while (y) {
        if (y & 1) {
            res = (1ll * res * x) % divv;
        }
        x = (1ll * x * x) % divv;
        y >>= 1;
    }
    return res;
}

bool is_integer (int x) {
    int res = 0;
    for (int i = 1; i <= tot; ++i) {
        res = (res + 1ll * coef[i] * fpow (x, ind[i])) % divv;
    }
    return res == 0;
}

signed main () {
//  freopen ("data.out", "w", stdout);
    while (cin >> str) {
        if (str[0] == '.') break;
        n = str.length (); tot = 0; 
        get_poly ();
        bool succ = true;
        for (int i = 1; i <= ind[1]+ 1; ++i) {
            if (!is_integer (i)) {
                succ = false; break;
            }
        }
        if (succ) {
            cout << "Case " << ++kase << ": Always an integer" << endl; 
        } else {
            cout << "Case " << ++kase << ": Not always an integer" << endl;
        }
    }
}

例题\(9\) 最大公约数之和——极限版 II(\(UVa11426\)

  • 题意:求\(\sum_{i = 1}^{N}\sum_{j = i + 1}^{N}gcd(i, j)\)

  • 显然有很多\(gcd(i, j)\)会等于\(1\),而且可以用欧拉函数的前缀和直接表示其个数。由此我们得到启示:可以枚举这个\(d\),除去\(d\)就变成了\(i,j\)这两个数互质的情况,就可以转化为欧拉函数来求了。

  • 函数式的思想:设\(f(n) = \sum_{i=1}^{N-1}gcd(i, n)\)\(g(n, i)\)为满足\(gcd (x, n) = i\)\(x < n\)\(x\)的个数。注意到\(gcd(x, n) = i\)的充要条件是\(gcd(\frac{x}{i}, \frac{n}{i}) = 1\),可以得到满足条件的\(\frac{x}{i}\)\(phi(\frac {n}{i})\)个,那么\(f(n)\)就可以枚举约数直接推导了。

  • 当然,\(f(n)\)约数的枚举可以线性筛时直接存储,复杂度会更好。

#include 
using namespace std;

#define phi sumphi
const int N = 4000010;
typedef long long LL;

int n, tot, vis[N], prime[N]; LL sumphi[N];

void get_phi () {
    vis[1] = true; phi[1] = 1;
    for (int i = 2; i <= N; ++i) {
        if (!vis[i]) {
            phi[i] = i - 1;
            prime[++tot] = i;
        }
        for (int j = 1; j <= tot && i * prime[j] < N; ++j) {
            vis[i * prime[j]] = true;
            if (i % prime[j] == 0) {
                phi[i * prime[j]] = phi[i] * prime[j]; 
                break;
            } else {
                phi[i * prime[j]] = phi[i] * (prime[j] - 1);
            }
        }
    } 
    for (int i = 1; i < N; ++i) sumphi[i] = sumphi[i - 1] + phi[i];
}


int main () {
    get_phi ();
    while (cin >> n && n) {
        LL ans = 0;
        for (int d = 1; d <= n; ++d) {
            ans += d * (sumphi[n / d] - 1);
        }
        cout << ans << endl;
    }
}

  • 线性同余方程\(ax≡c(mod\) \(b)\)的解法:转化为线性不定方程\(ax+by=c\),方程有解当且仅当\(gcd(a, b)|c\)

  • 线性不定方程\(ax+by=c\)的解法:把其中每个系数都除以\(gcd(a, b)\),得到\(a'x+b'y=c'\),可以用\(Exgcd\)求出模\(b'\)的解,转化为模\(b\)的解只需要乘以一个\([1, gcd(a, b)]\)的倍数就好。

  • 线性不定方程组的解法:(扩展)中国剩余定理,简单推一下式子即可。

  • 离散对数的解法:分块求解(\(BSGS\)),因为根据欧拉定理,\(AX≡1(Mod\) \(p)\)\(x\)如果有解,那么一定在\(p - 1\)的范围内。复杂度\(O(\sqrt N)\)


例题\(10\) 数论难题($UVa$11754)

  • 来跟我一起读:数(shù)论(lùn)难(jiǎ)题(

  • 如果\(\prod k\)不是特别大可以直接枚举每个集合里选择哪个余数,分别做\(CRT\)。如果\(\prod k\)比较大,那么似乎更容易在比较小的范围内出解。我们选择一个\(k/x\)最小的方程进行枚举。(\(k\)大是为了让每次枚举的答案跨度尽可能大,\(x\)小是让每次枚举的余数个数尽量少)因为\(s\)也不太大所以可行。

  • 复杂度\(O(\)能卡过去\()\)

#include 
using namespace std;

const int N = 10;
const int M = 100;
const int Lim = 10000;

#define int long long

int n, m, tot, mul, bestc, usey[N];

int x[N], k[N], y[N][M];

vector  sol;

void exgcd (int a, int b, int &x, int &y) {
    if (b == 0) {
        x = 1, y = 0; return;   
    }
    exgcd (b, a % b, x, y);
    int xx = y, yy = x - (a / b) * y;
    x = xx, y = yy;
}

int inv (int a, int b) {
    int x = 0, y = 0;
    exgcd (a, b, x, y);
//  cout << "inv " << a << "X = 1 (Mod " << b << ") = " << x << endl;
    return (((x % b) + b) % b);
}

int china () {
    int ret = 0;
    for (int i = 0; i < n; ++i) {
        ret = (ret + (mul / x[i]) * usey[i] * inv (mul / x[i], x[i])) % mul;
    }
//  cout << "china = " << ret << endl;
    return (((ret % mul) + mul) % mul);
}

void dfs (int now) {
    if (now == n) {
        sol.push_back (china ());
        return;
    }
    for (int i = 0; i < k[now]; ++i) {
        usey[now] = y[now][i];
        dfs (now + 1);
    }
}

void solve1 () {
    sol.clear ();
    dfs (0);
    sort (sol.begin (), sol.end ());
    for (int i = 0; m != 0; ++i) {
        for (int j = 0; j < sol.size (); ++j) {
            int res = mul * i + sol[j];
            if (res > 0) {
                cout << res << endl;
                if (--m == 0) break;
            }
        }
    }
}

set  values[N];

void solve2 () {
    for (int c = 0; c < n; ++c) if (c != bestc) {
        values[c].clear ();
        for (int i = 0; i < k[c]; ++i) {
            values[c].insert (y[c][i]);
        }
    }
    for (int t = 0; m != 0; ++t) {
        for (int i = 0; i < k[bestc]; ++i) {
            int res = t * x[bestc] + y[bestc][i];
            if (res == 0) continue;
            bool ok = true;
            for (int c = 0; c < n; ++c) if (c != bestc) {
                if (!values[c].count (res % x[c])) {
                    ok = false; break;
                }
            }
            if (ok) {cout << res << endl; if (--m == 0) break; }
        }
    }
}

signed main () {
//  freopen ("data.in", "r", stdin);
    while (cin >> n >> m) {
        if (n + m == 0) break;
        tot = 1, mul = 1, bestc = 0;
        for (int i = 0; i < n; ++i) {
            cin >> x[i] >> k[i];
            for (int j = 0; j < k[i]; ++j) {
                cin >> y[i][j];
            }
            sort (y[i], y[i] + k[i]);
            mul *= x[i], tot *= k[i];
            if (k[bestc] * x[i] > k[i] * x[bestc]) bestc = i;   
        }
        if (tot <= Lim) {
            solve1 ();
        } else {
            solve2 ();
        }
        printf ("\n");
    }
}

例题\(11\) 网格涂色(\(UVa11916\)

  • 先固定有限制的部分,下面的没有限制的部分每一行带来的答案贡献是一定的,可以\(BSGS\)出来。
#include 
using namespace std;

#define int long long
const int N = 500 + 5;
const int Mod = 1e8 + 7;

int T, n, m, k, b, r, x[N], y[N];

set  > bset;

int mul (int x, int y) {x %= Mod, y %= Mod; return (((1ll * x * y) % Mod) + Mod) % Mod;}

int mpow (int x, int y) {
    int res = 1;
    while (y) {
        if (y & 1) {
            res = mul (res, x);
        }
        x = mul (x, x);
        y >>= 1;
    } 
    return res;
}

map  mp;

int mlog (int A, int B) {// A^x = B (mod Mod)
    mp.clear ();
    int blo = sqrt (Mod) + 1, val = 1;
    for (int i = 0; i <= blo; ++i) {
        mp[mul(B, val)] = i;
        val = mul (val, A);// val = A ^ i 
    }
    int w = mpow (A, blo); val = 1;
    for (int i = 1; i <= blo; ++i) {
        val = mul (val, w); // val = A ^ {i * blo};
        if (mp.count (val)) {
            return i * blo - mp[val];
        }
    }
    return -1;
}

int inv (int x) {return mpow (x, Mod - 2);}

int count () {
    int c = 0;//有k种方法的个数
    for (int i = 1; i <= b; ++i) {
        if (x[i] != m && !bset.count (make_pair (x[i] + 1, y[i]))) c++;
    } 
    c += n;
    for (int i = 1; i <= b; ++i) if (x[i] == 1) c--;
    return mul (mpow (k, c), mpow (k - 1, n * m - b - c));
}

int solve () {
    int cnt = count ();
//  cout << "cnt = (1) " << cnt << endl;
    if (cnt == r) return m;
    
    int c = 0;
    for (int i = 1; i <= b; ++i) {
        if (x[i] == m) c++;
    }
    m++;
    cnt = mul (cnt, mpow (k, c));
    cnt = mul (cnt, mpow (k - 1, n - c));
//  cout << "cnt = (2) " << cnt << endl;
    if (cnt == r) return m;
    
    return mlog (mpow (k - 1, n), mul (r, inv (cnt))) + m;
}

signed main () {
//  freopen ("data.in", "r", stdin);
    cin >> T;
    for (int kase = 1; kase <= T; ++kase) {
        cin >> n >> k >> b >> r;
        bset.clear (); m = 1;
        for (int i = 1; i <= b; ++i) {
            cin >> x[i] >> y[i];
            if (x[i] > m) m = x[i];
            bset.insert (make_pair (x[i], y[i]));
        }
        cout << "Case " << kase << ": " << solve () << endl;
    }
} 

  • 公平组合游戏:

    • 两个游戏者轮流操作

    • 状态集合有限

    • 不能操作者输

  • 所有公平组合游戏都可以表示为有向图上进行转移的游戏。

  • 必胜状态与必败状态:

    • 一个状态必胜当且仅当其后有一个状态必败(逼死对手)

    • 一个状态必败当且仅当其后所有状态必胜(无路可去)

    • 作为边界条件,没有后继的状态是必败状态

  • 这样我们就可以直接进行有向图上博弈搜索了,但大多数博弈论的题目需要一些巧妙的结论。

  • \(Nim, Chomp, Ferguson\)游戏:略。

  • 解决组合游戏的有力工具:\(SG\)函数

    • 这里不做详细介绍,不懂请自行百度。
  • \(SG\)定理:组合游戏的和的\(SG\)函数等于其所有子游戏的\(SG\)函数的\(Nim\)和(异或和)。

  • \(SG\)函数的常见用法:打表找规律。

  • 翻棋子游戏:

    • 每个正面朝上的点(x, y)是一个子游戏 (x, y), 任意一次 xor 代表当前子游戏范围被减小 / 删除。

    • e.g (x, y) -- x ^ a --> (a, y),其中K ^= (x ^ a); 原本的堆 x 变成堆 a. 【进行子游戏 (x, y)。

    • e.g (x, y) and (a, y) -- x ^ a --> NULL,两者同时被删除。【把子游戏(x, y), (a, y) 重新组合为:子游戏 (x, a) 和 子游戏 (y, y),然后进行子游戏(x, a)。后者可以直接忽略。

    • 如果 a 不存在:因为 0 < a < x, 所以 (x ^ a) 一定 < x, 即子游戏 (x, y) 变成 子游戏 (x, a) 的过程。

    • 如果 a 存在:异或掉就变成了 0, 即 Nim 游戏中整堆取走的情况 。因为都是异或,所以坐标可以自由组合,等效于直接结束 (x, a) 这个子游戏。


例题\(12\) 石子游戏(\(LA5059\)

  • 打表找规律。。没啥可以说的。
#include 
using namespace std;

const int N = 100;
#define int long long

int T, n, w;

int sg (int x) {
    return x & 1 ? sg (x / 2) : x / 2;
}
signed main () {
//  freopen ("data.in", "r", stdin);
    cin >> T;
    while (T--) {
        cin >> n; int ans = 0;
        for (int i = 1; i <= n; ++i) {
            cin >> w; ans ^= sg (w);
        }
        puts (ans ? "YES" : "NO");
    }
}

例题\(13\)\(Treblecross\)游戏(\(UVa10561\)

  • 博弈搜索\(+\)\(SG函数\),考虑每个格子向左右扩展两个是不可用区域即可。

  • 代码用了一些细节避免特判。

#include 
using namespace std;

const int N = 2010;

int n, sg[N], vis[N];

int dfs (int x) {
    if (vis[x]) return sg[x];
    vis[x] = true; vector  v;
    for (int p = 1; p <= x; ++p) {
        v.push_back (dfs (max (p - 3, 0)) ^ dfs (max (x - p - 2, 0)));
    }
    sort (v.begin (), v.end ());
    int now = 0;
    for (int i = 0; i < v.size (); ++i, ++now) {
        if (v[i] != now) break;
        while (v[i + 1] == now) ++i;
    } 
    return sg[x] = now;
}

int T, sta[N]; char s[N];

int get_ans (int len, char *s) {
    int top = 0, ret = 0;
    for (int i = 0, tot = 0; i <= len; ++i) {
        if (s[i] == 'X') {
            sta[++top] = max (tot - 4, 0), tot = 0;
        } else ++tot;
    }
    for (int i = 1; i <= top; ++i) ret ^= sg[sta[i]];
    return ret;
}

int res[N];

bool special_judge () {
    int top = 0;
    for (int i = 3; i <= n - 3; ++i) {
             if (s[i - 1] == 'X' && s[i - 2] == 'X') res[++top] = i - 2;
        else if (s[i + 1] == 'X' && s[i + 2] == 'X') res[++top] = i - 2;
        else if (s[i - 1] == 'X' && s[i + 1] == 'X') res[++top] = i - 2;
    }
    if (top != 0) {
        cout << "WINNING" << endl;
        for (int i = 1; i <= top; ++i) {
            cout << res[i]; if (i != top) putchar (' ');
        }
        putchar ('\n');
        return true;
    } else return false;
}

bool can_use (int x) {
    return s[x - 2] != 'X' && s[x - 1] != 'X' && s[x] != 'X' && s[x + 1] != 'X' && s[x + 2] != 'X';
}

int main () {
    sg[0] = 0, vis[0] = true;
    for (int i = 1; i < N; ++i) dfs (i);
    cin >> T;
    while (T--) {
        cin >> (s + 3); n = strlen (s + 3) + 2;
        s[0] = 'X', s[1] = '.', s[2] = '.';
        s[++n] = '.', s[++n] = '.', s[++n] = 'X';
        if (special_judge ()) continue;
        int ans = get_ans (n, s);
        if (ans == 0) {
            cout << "LOSING" << endl << endl;
        } else {
            int top = 0;
            cout << "WINNING" << endl;
            for (int i = 3; i <= n - 3; ++i) {
                if (!can_use (i)) continue;
                s[i] = 'X';
                if (get_ans (n, s) == 0) {
                    res[++top] = i - 2;
                }
                s[i] = '.';
            }
            for (int i = 1; i <= top; ++i) {
                cout << res[i]; if (i != top) putchar (' ');
            }
            putchar ('\n');
        }
//      for (int i = 1; i <= top; ++i) cout << sta[i] << " "; putchar ('\n');
    }
}

  • 全概率公式:把样本空间\(S\)分割成互不相交的若干部分\(B_1, B_2...B_n\), 则\(P(A)=P(A|B_1) * P(B_1) + ... + P(A|B_n) * P(B_n)\)

    • 关键:不重不漏地对样本空间分类,并计算每个分类下的事件概率。
  • 全期望公式:类似全概率公式,不重不漏地分类加权。

    • 在解决和数学期望相关的题目时,可以优先考虑上全期望公式。
  • 写题思想:由抽象到具体,从大到小,不重不漏,依次划分。


例题\(14\) 麻球繁衍(\(UVa11021\)

  • 打公式很累,请看蓝书。讲解还是相当清晰的。
#include 
using namespace std;

const int N = 1000 + 5;
typedef double ldb;

int T, n, m, k; ldb P[N], f[N];

ldb fpow (ldb x, int y) {
    ldb res = 1;
    while (y) {
        if (y & 1) {
            res = res * x;
        }
        x = x * x;
        y >>= 1;
    }
    return res;
}

int main () {
//  freopen ("data.in", "r", stdin);
    cin >> T;
    for (int kase = 1; kase <= T; ++kase) {
        cin >> n >> k >> m;
        for (int i = 0; i < n; ++i) cin >> P[i];
        f[1] = P[0];
        for (int i = 2; i <= m; ++i) {
            f[i] = 0;
            for (int j = 0; j < n; ++j) {
                f[i] += P[j] * fpow (f[i - 1], j);
            }
        }
        printf ("Case #%d: %.7lf\n", kase, fpow (f[m], k));
    }
}   

例题\(15\)和朋友会面(\(UVa11722\)

  • 写法:大特判大模拟

  • 为了代替上面那种毒瘤方法,可以求出来\(y = x + w\)\(y = x - w\)与两人时间区间矩形(\(x = t1, x = t2, y = s1, y = s2\))的交点,然后求上下两个凸多边形面积,比上总面积就是答案。

  • ~~完全就是把一坨翔变成另一坨翔=_=~~

#include 
using namespace std;

const int N = 1000;

struct Node {
    int x, y;
    
    bool operator < (Node rhs) const {
        return x == rhs.x ? y < rhs.y : x < rhs.x;
    }
    
    bool operator == (Node rhs) const {
        return x == rhs.x && y == rhs.y;
    }
}nd[N], arr1[N], arr2[N];

int T, t1, t2, s1, s2, w;

void get_arr (int w, Node *arr, int &tot) {
    if (s1 <= t1 + w && t1 + w <= s2) arr[++tot] = (Node) {t1, t1 + w};
    if (s1 <= t2 + w && t2 + w <= s2) arr[++tot] = (Node) {t2, t2 + w}; 
    if (t1 <= s1 - w && s1 - w <= t2) arr[++tot] = (Node) {s1 - w, s1};
    if (t1 <= s2 - w && s2 - w <= t2) arr[++tot] = (Node) {s2 - w, s2};
    if (s1 <= t1 + w && t1 + w <= s2) if (w > 0) arr[++tot] = nd[2]; else arr[++tot] = nd[1], arr[++tot] = nd[3];
    if (s1 <= t2 + w && t2 + w <= s2) if (w > 0) arr[++tot] = nd[2], arr[++tot] = nd[4]; else arr[++tot] = nd[3];
    if (t1 <= s1 - w && s1 - w <= t2) if (w > 0) arr[++tot] = nd[1], arr[++tot] = nd[2]; else arr[++tot] = nd[3];
    if (t1 <= s2 - w && s2 - w <= t2) if (w > 0) arr[++tot] = nd[2]; else arr[++tot] = nd[3], arr[++tot] = nd[4];
    sort (arr + 1, arr + tot + 1);
    tot = unique (arr + 1, arr + tot + 1) - arr - 1;
}

Node base;

int disPP (Node lhs, Node rhs) {
    return hypot (abs (lhs.x - rhs.x), abs (lhs.y - rhs.y));
}

int cross (Node u1, Node u2, Node v1, Node v2) {
    return (u2.x - u1.x) * (v2.y - v1.y) - (v2.x - v1.x) * (u2.y - u1.y);
}

bool cmp (Node lhs, Node rhs) {
    if (cross (base, lhs, base, rhs) < 0) return false;
    if (cross (base, lhs, base, rhs) > 0) return true;
    return disPP (base, lhs) < disPP (base, rhs);
}

double getS (Node *arr, int tot) {
    if (tot < 3) return 0; 
    for (int i = 1; i <= tot; ++i) {
        if (arr[i].y < arr[1].y) {
            swap (arr[i], arr[1]);
        }
    }
    base = arr[1];
    sort (arr + 2, arr + 1 + tot, cmp);
    double ret = 0;
    for (int i = 2; i < tot; ++i) {
        ret += cross (base, arr[i], base, arr[i + 1]) / 2.0;
    }
    return ret;
}

int main () {
//  freopen ("data.in", "r", stdin);
//  freopen ("data.out", "w", stdout);
    cin >> T;
    for (int kase = 1; kase <= T; ++kase) {
        cin >> t1 >> t2 >> s1 >> s2 >> w;
        nd[1] = (Node) {t1, s1}, nd[2] = (Node) {t1, s2};
        nd[3] = (Node) {t2, s1}, nd[4] = (Node) {t2, s2};
        int tot1 = 0, tot2 = 0;
        get_arr (+w, arr1, tot1);
        get_arr (-w, arr2, tot2);
        double ans = 1.0 - (getS (arr1, tot1) + getS (arr2, tot2)) / (1.0 * (t2 - t1) * (s2 - s1)); 
        if (-w > s2 - t1 || +w < s1 - t2) ans =  0;
        printf ("Case #%d: %.8lf\n", kase, ans);
    }
} 

例题\(16\) 玩纸牌(\(UVa11427\)

  • 每天晚上都是等效的,先研究一个晚上的情况,发现可以\(DP\)求每个晚上继续喝不继续的概率,那么直接上全期望公式就完事了。
#include 
using namespace std;

const int N = 100 + 5;

int T, n; double p, dp[N][N];

int read () {
    int s = 0, ch = getchar ();
    while ('9' < ch || ch < '0') {
        ch = getchar ();
    }
    while ('0' <= ch && ch <= '9') {
        s = s * 10 + ch - '0';
        ch = getchar ();
    }
    return s;
}

int main () {
//  freopen ("data.in", "r", stdin);
    T = read ();
    for (int kase = 1; kase <= T; ++kase) {
        memset (dp, 0, sizeof (dp));
        p = read (), p /= read (), n = read ();
        dp[0][0] = 1;
        for (int i = 1;  i <= n; ++i) {
            for (int j = 0; j <= i * p; ++j) {
                dp[i][j] += dp[i - 1][j] * (1 - p) ;
                if (j != 0) dp[i][j] += dp[i - 1][j - 1] * p;
            }
        }
        double p0 = 0;
        for (int j = 0; j <= n * p; ++j) p0 += dp[n][j];
        printf ("Case #%d: %d\n", kase, (int) (1 / p0));
    }
}

例题\(17\) 得到\(1\)\(UVa11762\)

  • 直接上全期望公式然后记搜,没啥难的=_=

  • 如果不明白看我代码就可以了。很好懂的。

#include 
using namespace std;

const int N = 1000000 + 5;

vector  p[N];

int T, n, f[N], g[N], vis[N], used[N]; double res[N];

void get_prime () {
    vis[1] = true;
    for (int i = 2; i < N; ++i) {
        if (!vis[i]) {
            for (int j = i; j < N; j += i) {
                p[j].push_back (i); 
                g[j]++; if (j != i) vis[j] = true;
            }
        }
    }
    for (int i = 1; i < N; ++i) {
        f[i] = f[i - 1] + (vis[i] == 0);
    }
}

double E (int x) {
    if (used[x]) return res[x];
    used[x] = true;
    double ret = f[x] - g[x];
    for (int i = 0; i < g[x]; ++i) {
        ret += E (x / p[x][i]) + 1;
    }
    return res[x] = ret / g[x]; 
} 

int main () {
//  freopen ("data.in", "r", stdin);
    cin >> T;
    get_prime ();
    used[1] = true; res[1] = 0;
    for (int kase = 1; kase <= T; ++kase) {
        cin >> n;
        printf ("Case %d: %.10lf\n", kase, E (n));
    }
}

  • 置换:可以理解为序列之间的一一映射。

  • 置换乘法:就是先搞第一个置换,再搞第二个置换。不满足交换律,即\(A * B != B * A\)

  • 置换的循环分解:

    • 可以证明,任意一个置换一定可以分解为循环乘积的形式。

    • 就是说,可以分成几个圈,每一个圈里面的数在进行这个置换时都只会在这个圈里绕。

    • 循环的乘积:在循环\(A\)里往前推一位,跳到循环\(B\)里再往前推一位。

    • e.g:

      • \(A = (a1, a2, a3)(b1, b2, b3, b4)\)

      • \(A^2 = (a1, a2, a3)(a1, a2, a3)(b1, b2, b3, b4)(b1, b2, b3, b4)\)

      • 根据置换乘法的结合律,前面两个结合,后面两个结合。

      • \(A^2 = (a1, a3, a2)(b1, b3)(b2, b4)\)

  • 等价类计数问题:

    • 定义一种等价关系,求等价类的个数。

    • 首先用置换集合\(F\)描述等价关系。(\(F\)中包含所有可能的置换)

    • 定义\(C(f)\)是置换\(f\)的不动点个数,那么置换集合\(F\)的等价类个数就是\(\frac{1}{|F|} * \sum C_i\)

    • 上面这个结论被称为\(Burnside\)引理。这里有我写过的\(Burnside\)引理的证明。

    • \(Polya\)引理可以很容易拿前面的推出来。设置换\(f\)\(m(f)\)个循环,每个点可以涂\(k\)种颜色,那么每个循环内每个点应该颜色相同。由乘法原理有\(C(f) = k^{m(f)}\)


例题\(18\) 项链和手镯(\(UVa10294\)

  • 讲解还是翻蓝书吧\(QwQ\),蓝书上讲的真的很详细很优美了。
#include 
using namespace std;

#define int long long

int fpow (int x, int y) {
    int res = 1;
    while (y) {
        if (y & 1) {
            res = res * x;
        }
        x = x * x;
        y >>= 1;
    }
    return res;
}

int gcd (int x, int y) {
    return y ? gcd (y, x % y) : x;
}

int n, t;

signed main () {
//  freopen ("data.in", "r", stdin);
    while (cin >> n >> t) {
        int ans1 = 0, ans2 = 0, tot1 = 0, tot2 = 0;
        for (int i = 0; i <= n - 1; ++i) {
//          cout << "gcd (i, n) = " << gcd (i, n) << endl; 
            ans1 += fpow (t, gcd (i, n));
        }
        tot1 = tot2 = n;
        if (n & 1) {
            tot2 += n;
            ans2 = n * fpow (t, (n + 1) / 2);
        } else {
            tot2 += n;
            ans2 = n / 2 * fpow (t, n / 2) + n / 2 * fpow (t, n / 2 + 1);
        }
        cout << ans1 / tot1 << " " << (ans1 + ans2) / tot2 << endl;
    }
}

例题\(19\)\(Leonardo\)的笔记本(\(LA3641\)

  • 推论:

    • 偶数长度的循环相乘,生成两个长度折半的循环。

    • 奇数长度的循环相乘,生成一个长度相同的循环。

  • 偶数长度的循环只能通过偶数长度的循环乘出来,必须两两配对才行。只要循环长度对得上,构造一个置换还是很容易的。

#include 
using namespace std;

const int N = 30;

int T, top, B[N], vis[N], cir[N]; char s[N];

int main () {
    cin >> T;
    while (T--) {
        cin >> s; top = 0;
        memset (cir, 0, sizeof (cir));
        memset (vis, 0, sizeof (vis));
        for (int i = 0; i < 26; ++i) {
            B[s[i] - 'A'] = i;
        }
        for (int i = 0; i < 26; ++i) {
            if (!vis[i]) {
                vis[i] = true;
                int p = B[i], sz = 1;
                while (p != i) {
                    vis[p] = true;
                    sz++, p = B[p];
                }
//              cout << "sz = " << sz << endl;
                if (sz % 2 == 0) cir[sz]++;
            }
        }
        bool failed = false;
        for (int i = 0; i < 26; ++i) {
            if (cir[i] & 1) {
                failed = true;
                break;
            }
        }
        puts (failed ? "No" : "Yes");
    }
}

例题\(20\) 排列统计(\(UVa11077\)

  • 单元素循环不需要交换,\(k\)元素循环需要交换\(k - 1\)次。如果排列的循环节为\(x\),那么交换次数就是\(n - x\)

  • 然后设\(f(i, j)\)为至少交换\(j\)次才能变成\(1->i\)的排列个数。每次新增一个元素\(i\)进去,可以加入前面任意一个循环的任意一个位置(一共\(i - 1\)个位置)使交换次数\(+1\),也可以新成一个循环不消耗交换次数。

  • \(f(i, j) = f(i - 1, j - 1) + f (i - 1, j) * (i - 1)\)

#include 
using namespace std;

const int N = 22;
#define uint unsigned long long

uint n, k, dp[N][N];

int main () {
    dp[1][0] = 1;
    for (int i = 2; i < N; ++i) {
        for (int j = 0; j <= i; ++j) {
            dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1] * (i - 1);
            //dp[i][[j] -> 前i个元素,交换j次得到
            //dp[i - 1][j] -> 第i个新成一个循环
            //dp[i - 1][j - 1] * (i - 1) -> 第i个元素插入到前面(i - 1)个元素的位置上去(到已有循环里)交换次数+1 
        }
    }
    while (cin >> n >> k) {
        if (n + k == 0) break; 
        cout << dp[n][k] << endl;
    }
}

例题\(21\) 像素混合(\(LA3510\)

  • 学术不精,置换基本上没怎么写过题目,所以暂时弃了。回头有空我会补一下\(bzoj\)置换的题目,到时候再把这个题目写掉。

  • 矩阵:数排列成的矩形。第\(i\)行第\(j\)列的元素是\(A_{ij}\)
    • 运算:
      • 加法:直接相加。

      • 减法:直接相减。

      • 乘法:
        • 运算矩阵:\(lhs * rhs\)

        • 运算条件:\(lhs\)\(n * m\)的矩阵,\(rhs\)\(m * r\)的矩阵,即左矩阵列数等于右矩阵行数。

        • 运算规则: \(A_{i,k} = \sum lhs_{i, j} * rhs_{j, k}\)

        • 运算原理:想象抽离左矩阵的第\(i\)行,转成竖着的塞进右边矩阵的第\(k\)列,让它们一一对应相乘。

        • 运算实质:元素的线性组合。

    • 由于矩阵乘法满足结合律,所以可以使用倍增加速。通常用于加速线性关系的递推。
  • 高斯消元:用来解方程组或者求矩阵的秩(可以自由取值的变量个数)。

    • 一般用高斯约旦消元法就好。

例题\(22\) 递推关系(\(Uva10870\)

  • 直接构造矩阵就好 \(=w=\) 没什么技术含量
#include 
using namespace std;

const int N = 17;

struct Matrix {
    int a[N][N];
    
    Matrix () {memset (a, 0, sizeof (a));}
    
    void Unit () {for (int i = 0; i < N; ++i) a[i][i] = 1;}
};

int d, n, m, a[N], f[N];

int mul (int x, int y) {return (((1ll * x * y) % m) + m) % m;}
int add (int x, int y) {return (((0ll + x + y) % m) + m) % m;}

Matrix Mat_mul (Matrix m1, Matrix m2) {
    Matrix res;
    for (int i = 1; i < N; ++i) {
        for (int j = 1; j < N; ++j) {
            for (int k = 1; k < N; ++k) {
                res.a[i][k] = add (res.a[i][k], mul (m1.a[i][j], m2.a[j][k]));
            }   
        }
    }
    return res;
}

Matrix Mat_pow (Matrix mat, int p) {
    Matrix res; res.Unit ();
    while (p != 0) {
        if (p & 1) {
            res = Mat_mul (res, mat);
        }
        mat = Mat_mul (mat, mat);
        p >>= 1; 
    }
    return res;
}

Matrix ini, mat;

int main () {
//  freopen ("data.in", "r", stdin);
    while (cin >> d >> n >> m) {
        if (d + n + m == 0) break;
        for (int i = 1; i <= d; ++i) cin >> a[i];
        for (int i = 1; i <= d; ++i) cin >> f[i];
        memset (ini.a, 0, sizeof (ini.a));
        memset (mat.a, 0, sizeof (mat.a));
        for (int i = 1; i <= d; ++i) ini.a[1][i] = f[i];
        for (int i = 2; i <= d; ++i) mat.a[i][i - 1] = 1;
        for (int i = 1; i <= d; ++i) mat.a[i][d] = a[d - i + 1];
        Matrix res = Mat_mul (ini, Mat_pow (mat, n - d)); 
        cout << res.a[1][d] << endl;
    }
}

例题\(23\) 细胞自动机(\(LA3704\)

  • 新科技:循环矩阵。

    • 循环矩阵 * 循环矩阵还是循环矩阵

    • 循环矩阵的第\(i + 1\)行等于其第\(i\)行集体往后推一位(推出界就回到第一列)

    • 所以每次矩阵乘法的时候算第一行就好啦~~复杂度\(O(N^2logN)\)

#include 
using namespace std;

const int N = 500 + 5;

struct Matrix {
    int a[N][N];
    
    Matrix () {memset (a, 0, sizeof (a));}
    
    void Unit () {for (int i = 0; i < N; ++i) a[i][i] = 1;}
};

int n, m, d, k, arr[N];

int add (int x, int y) {return (((0ll + x + y) % m) + m) % m;}
int mul (int x, int y) {return (((1ll * x * y) % m) + m) % m;}

Matrix Mat_mul (Matrix m1, Matrix m2, int r) {
    Matrix res;
    for (int i = 0; i < r; ++i) {
        for (int j = 0; j < n; ++j) {
            for (int k = 0; k < n; ++k) {
                res.a[i][k] = add (res.a[i][k], mul (m1.a[i][j], m2.a[j][k]));
            }
        }
    }
    return res;
}

Matrix cir_mul (Matrix m1, Matrix m2) {
    Matrix res;
    for (int i = 0; i < 1; ++i) {
        for (int j = 0; j < n; ++j) {
            for (int k = 0; k < n; ++k) {
                res.a[i][k] = add (res.a[i][k], mul (m1.a[i][j], m2.a[j][k]));
            }
        }
    }
    for (int i = 1; i < n; ++i) {
        for (int j = 1; j < n; ++j) {
            res.a[i][j] = res.a[i - 1][j - 1];
        }
        res.a[i][0] = res.a[i - 1][n - 1];
    }
    return res;
}

Matrix Mat_pow (Matrix mat, int p) {
    Matrix res; res.Unit ();
    while (p != 0) {
        if (p & 1) {
            res = cir_mul (res, mat);
        }
        mat = cir_mul (mat, mat);
        p >>= 1;
    }
    return res;
}

Matrix ini, mat;

int main () {
//  freopen ("data.in", "r", stdin);
    while (cin >> n >> m >> d >> k) {
        memset (ini.a, 0, sizeof (ini.a));
        memset (mat.a, 0, sizeof (mat.a));
        for (int i = 0; i < n; ++i) cin >> arr[i];
        for (int i = 0; i < n; ++i) ini.a[0][i] = arr[i];
        for (int j = 0; j < n; ++j) {
            mat.a[j][j] = 1; 
            int s1 = d, s2 = d;
            int p1 = (j + 1 + n) % n, p2 = (j - 1 + n) % n;
            while (s1--) {mat.a[p1][j] = 1, p1 = (p1 + 1 + n) % n;}
            while (s2--) {mat.a[p2][j] = 1, p2 = (p2 - 1 + n) % n;}
        }
        Matrix res = Mat_mul (ini, Mat_pow (mat, k), 1);
        for (int i = 0; i < n; ++i) cout << (i == 0 ? "" : " ") << res.a[0][i]; cout << endl;
    }
}

例题\(24\) 随机程序(\(UVa10828\)

  • 思维上类似于之前那个期望题目,直接上全期望公式。问题在于这个图里有环,所以就必须要用高斯消元来解决状态之间的依赖关系。

  • 如果不存在死循环就好办了\(=\_=\),但死循环这个东西让程序变得极其麻烦。

  • \(Plan\) \(A\):提前求一次传递闭包,去除不可用的点,重新编号跑\(Gauss\)

    • 优点:不容易出锅

    • 缺点:码

  • \(Plan\) \(B\):用魔改的高斯消元,遇见消不了的行就跳,出来再特判。

    • 优点:短

    • 缺点:容易出锅。

  • 这里只提供第二种。

#include 
using namespace std;

const int N = 100 + 5;
const double eps = 1e-8;

void gauss_jordan (double A[N][N], int n) {
    for (int i = 0; i < n; ++i) {
        int r = i;
        for (int j = i + 1; j < n; ++j) {
            if (fabs (A[j][i]) > fabs (A[r][i])) r = j;
        }
        if (fabs (A[r][i]) < eps) continue;
        if (r != i) swap (A[r], A[i]);
        
        for (int k = 0; k < n; ++k) {
            if (k != i) {
                for (int j = n; j >= i; --j) {
                    A[k][j] -= A[k][i] / A[i][i] * A[i][j];
                }
            }
        }
    }
}

double A[N][N];

vector  pre[N];

int n, u, v, d[N]; bool inf[N];

int main () {
    int kase = 0;
    while (cin >> n && n) {
        memset (d, 0, sizeof (d));
        for (int i = 0; i < n; ++i) pre[i].clear ();
        while (cin >> u >> v && u) {
            u--, v--, d[u]++;
            pre[v].push_back (u);
        }
        memset (A, 0, sizeof (A));
        for (int i = 0; i < n; ++i) {
            A[i][i] = 1;
            for (int j = 0; j < pre[i].size (); ++j) {
                A[i][pre[i][j]] -= 1.0 / d[pre[i][j]];
            }
            if (i == 0) A[i][n] = 1;
        }
        gauss_jordan (A, n);
        memset (inf, 0, sizeof (inf));
        for (int i = n - 1; i >= 0; --i) {
            if (fabs (A[i][i]) < eps && fabs (A[i][n]) > eps) inf[i] = 1;
            for (int j = i + 1; j < n; ++j) {
                if (fabs (A[i][j]) > eps && inf[j]) inf[i] = 1;
            }
        }
        
        int q, u;
        cin >> q;
        cout << "Case #" << ++kase << ":" << endl;
        while (q--) {
            cin >> u; u--;
            if (inf[u]) puts ("infinity");
            else {
                printf ("%.3lf\n", fabs (A[u][u]) < eps ? 0.0 : A[u][n] / A[u][u]);
            }
        }
    }
}

例题\(25\) 乘积是平方数(\(UVa11542\)

  • 显然要对每个数分解质因子。保证是平方数的话,只需要考虑每个质因子次数的奇偶性就可以,不需要保存全部的次数。

  • 这样就得到了一个\(01\)的异或方程组(右边答案是\(0\))。求一下矩阵的秩(可以自由选择的元素)\(k\),每个变量可以选\(0\)\(1\), 除去全\(0\)的情况,那么\(2^k - 1\)就是答案。

#include 
using namespace std;

const int N = 100 + 5;
const int M = 500 + 5;
#define int long long

int T, n, arr[N], mat[N][M];

int tot, vis[N], prime[N];

void get_prime (int n) {
    vis[1] = true;
    for (int i = 2; i <= n; ++i) {
        if (!vis[i]) prime[++tot] = i;
        for (int j = 1; j <= tot && i * prime[j] <= n; ++j) {
            vis[i * prime[j]] = true;
            if (i % prime[j] == 0) break;
        }
    }
}

int Rank (int r, int c) {
    int i = 1, j = 1;
    while (i <= r && j <= c) {
        int p = i;
        for (int k = i; k <= r; ++k) {
            if (mat[k][j]) {p = k; break;}
        }
//      cout << "p = " << p << endl; 
        if (mat[p][j]) {
            if (p != i) swap (mat[p], mat[i]);
            for (int u = i + 1; u <= r; ++u) {
                if (mat[u][j]) {
                    for (int k = i; k <= c; ++k) {
                        mat[u][k] ^= mat[i][k];
                    }
                }
            }
            i = i + 1;
        }
        j = j + 1;
    }
//  cout << "i = " << i << endl;
    return i - 1;
}

signed main () {
//  freopen ("data.in", "r", stdin);
    cin >> T;
    get_prime (500);
    while (T--) {
        cin >> n;
        int maxp = 0;
        memset (mat, 0, sizeof (mat));
        for (int i = 1; i <= n; ++i) {
            cin >> arr[i];
            for (int j = 1; j <= tot; ++j) {
                if (arr[i] % prime[j] != 0) continue;
                maxp = max (maxp, j);
                while (arr[i] % prime[j] == 0) {
                    mat[j][i] ^= 1;
                    arr[i] /= prime[j];
                }
            }
        }
//      for (int i = 1; i <= n; ++i) {
//          for (int j = 1; j <= 4 + 1; ++j) {
//              cout << mat[i][j] << " ";   
//          }
//          cout << endl;
//      }
        cout << (1ll << (n - Rank (maxp + 1, n))) - 1 << endl;
    }
}

听说用\(bitset\)还会更快


数值方法简介:没啥可介绍的。

  • 实数二分:没啥好说的。

  • 实数三分:求单峰函数 / 单谷函数的极值。

  • 自适应\(Simpson\)积分:粗略地求求一个函数的积分。可以用来计算几何骗分。


例题\(26\) 解方程(\(UVa10431\)

  • 原函数具有单调性。实数二分。
#include 
using namespace std;

const double eps = 1e-8;

int p, q, r, s, t, u;

double f (double x) {
    return p * exp (-x) + q * sin (x) + r * cos (x) + s * tan (x) + t * x * x + u;
}

int main () {
//  freopen ("data.in", "r", stdin);
    while (cin >> p >> q >> r >> s >> t >> u) {
        double l = 0, r = 1;
        while (r - l > eps) {
            double mid = (l + r) / 2.0;
            if (f (mid) > eps) { //f(x) > 0
                l = mid;
            } else {
                r = mid;
            }
        }
//      cout << f(l) << endl;
        if (fabs (f (l)) > 1e-4) {
            puts ("No solution");
        } else {
            cout << fixed << setprecision (4) << l << endl;
        }
    }
}

例题\(27\) 误差曲线(\(LA5009\)

  • 单谷函数的最优势覆盖还是单谷函数。实数三分。
#include 
using namespace std;

const int N = 10000 + 5;

int T, n, a[N], b[N], c[N];

double F(double x) {
    double ans = -1e50;
    for (int i = 1; i <= n; ++i) {
        ans = max (ans, a[i] * x * x + b[i] * x + c[i]);
    }
    return ans;
}

double get_ans () {
    double l = 0, r = 1000;
    for (int i = 0; i < 100; ++i) {
        double mid1 = l + (r - l) / 3.0;
        double mid2 = r - (r - l) / 3.0;
        if (F (mid1) < F (mid2)) {
            r = mid2;
        } else {
            l = mid1;
        }
    }
    return F(l);
}

int main () {
//  freopen ("data.in", "r", stdin);
    cin >> T;
    while (T--) {
        cin >> n;
        for (int i = 1; i <= n; ++i) cin >> a[i] >> b[i] >> c[i];
        cout << fixed << setprecision (4) << get_ans () << endl;
    }
}

QQ:757973845。博主学习时比较仓促,博客中不清晰处,错误之处,还请指正:)

转载于:https://www.cnblogs.com/maomao9173/p/10721029.html

你可能感兴趣的:(【算法竞赛入门经典—训练指南】学习笔记(含例题代码与思路)第二章:数学基础...)