标签: 数学

[AHOI2009]中国象棋 题解

[AHOI2009]中国象棋 题解

题目地址:洛谷:【P2051】[AHOI2009]中国象棋 – 洛谷、BZOJ:Problem 1801. — [Ahoi2009]chess 中国象棋

题目描述

这次小可可想解决的难题和中国象棋有关,在一个N行M列的棋盘上,让你放若干个炮(可以是0个),使得没有一个炮可以攻击到另一个炮,请问有多少种放置方法。大家肯定很清楚,在中国象棋中炮的行走方式是:一个炮攻击到另一个炮,当且仅当它们在同一行或同一列中,且它们之间恰好 有一个棋子。你也来和小可可一起锻炼一下思维吧!

输入输出格式

输入格式:
一行包含两个整数N,M,之间由一个空格隔开。

输出格式:
总共的方案数,由于该值可能很大,只需给出方案数模9999973的结果。

输入输出样例

输入样例#1:

1 3

输出样例#1:

7

说明

样例说明
除了3个格子里都塞满了炮以外,其它方案都是可行的,所以一共有222-1=7种方案。

数据范围
100%的数据中N和M均不超过100
50%的数据中N和M至少有一个数不超过8
30%的数据中N和M均不超过6

题解

这道题在状态设计出来以后就没什么了。显然每行每列的炮不能超过2个,用dp[i][j][k]表示正在排第i行,有j列有1个炮,有k列有2个炮的状态。下面是若干转移方程。
①这一次不填炮
dp[i][j][k] += dp[i-1][j][k]
②这一次填1个炮,填在没炮的列,乘法原理选择一列
dp[i][j+1][k] += dp[i-1][j][k] * (m - j - k)
③这一次填1个炮,填在本来有1个炮的列,乘法原理选择一列
dp[i][j-1][k+1] += dp[i-1][j][k] * j
④这一次填2个炮,都填在没有填过的列,组合选择2列
dp[i][j+2][k] += dp[i-1][j][k] * C_{m-j-k}^2
⑤这一次填2个炮,分别填在没填过和有1个炮的列,乘法原理分别选择
dp[i][j][k+1] += dp[i-1][j][k] * (m - j - k) * j
⑥这一次填2个炮,都填在有1个炮的列,组合选择2列
dp[i][j+2][k] += dp[i-1][j][k] * C_j^2
这些写全了再对填完n列的各种情况加和即可。最后滚动数组优化下空间。

代码

注:这个式子比较长建议复制出去看或者看上面的式子。

// Code by KSkun, 2018/2
#include <cstdio>
#include <cstring>
typedef long long LL;

const int MO = 9999973;

int n, m;
LL dp[2][105][105], ans = 0;

inline LL cal(LL x) {
    return x * (x - 1) / 2;
}

inline int g(int x) {
    return x & 1;
} 

int main() {
    scanf("%d%d", &n, &m);
    dp[0][0][0] = 1;
    for(int i = 1; i <= n; i++) { // i行 
        for(int j = 0; j <= m; j++) { // 枚举1棋列 
            for(int k = 0; j + k <= m; k++) { // 枚举2棋列 
                if(dp[g(i - 1)][j][k]) {
                    //noif
                        dp[g(i)][j][k] = (dp[g(i)][j][k] + dp[g(i - 1)][j][k]) % MO; // 不放 
                    if(m - j - k > 0) 
                        dp[g(i)][j + 1][k] = (dp[g(i)][j + 1][k] + dp[g(i - 1)][j][k] * (m - j - k)) % MO; // 放1 0棋
                    if(j > 0) 
                        dp[g(i)][j - 1][k + 1] = (dp[g(i)][j - 1][k + 1] + dp[g(i - 1)][j][k] * j) % MO; // 放1 1棋
                    if(m - j - k > 1) 
                        dp[g(i)][j + 2][k] = (dp[g(i)][j + 2][k] + dp[g(i - 1)][j][k] * cal(m - j - k)) % MO; // 放2 0|0
                    if(m - j - k > 0 && j > 0) 
                        dp[g(i)][j][k + 1] = (dp[g(i)][j][k + 1] + dp[g(i - 1)][j][k] * (m - j - k) * j) % MO; // 放2 1|0
                    if(j > 1) 
                        dp[g(i)][j - 2][k + 2] = (dp[g(i)][j - 2][k + 2] + dp[g(i - 1)][j][k] * cal(j)) % MO; // 放2 1|1 
                }
            }
        }
        memset(dp[g(i + 1)], 0, sizeof dp[g(i + 1)]);
    }
    for(int i = 0; i <= m; i++) {
        for(int j = 0; i + j <= m; j++) {
            ans = (ans + dp[g(n)][i][j]) % MO;
        }
    }
    printf("%lld", ans);
    return 0;
}
NOIP2017提高组题解&总结

NOIP2017提高组题解&总结

本篇题解借鉴和参考了洛谷题解区的同学所写的题解以及部分博文,由于未能记录全面,并没能将这些参考内容全部列出,为此对这些内容的作者感到抱歉。

NOIP2017已经结束啦。在华中科大计科实验室度过的7个小时犯了挺多的错误,现在想想,其实自己能上300。初次考试,题型有少许变化加上自己没有经验,成功爆炸。还好最后D1T2救了一下场。
接下来是题解和感想。

NOIP2017 提高组 Day1

吐槽

T1实在是太太太太太太拉分了orz

个人成绩

30,90,0

题目列表(评测来自洛谷)

赛题 #A: P3951 小凯的疑惑 | 数论,找规律
赛题 #B: P3952 时间复杂度 | 大模拟,耐心
赛题 #C: P3953 逛公园 | 图论,记忆化搜索,k短路计数

小凯的疑惑

一句话题意

求使px+qy=n无自然数范围内的解的最大正整数n。其中,p, q互质。

解法

100% 1 \leq a, b \leq 10^9

只需输出pq - p - q即可。
证明:试使用反证法
假设存在正整数x, y使px + qy = pq - p - q
则有p(x+1)+q(y+1)=pq
易知p|q(y+1)
\because gcd(p, q)=1
\therefore p|y+1
同理q|x+1
y+1=pj, x+1=qk
则有pqk+qpj=pq
也就是pq(j+k)=pq
\because j+k=x+y+2 \geq 2
上式并不成立,得证。
但是考场上推出这个方法应该打表找规律。
还有一种扩欧推出的方法,更多题解可以看洛谷题解 P3951 – 百科 – 洛谷

代码

// Code by KSkun, 2017/11
#include <cstdio>

int main() {
    int a, b;
    scanf("%d%d", &a, &b);
    printf("%lld", 1ll * a * b - a - b);
    return 0;
}

总结

千言万语汇成一句话:数论推个屁,打表找规律。铭记在心,终生受用。

时间复杂度

没有概括,自己看洛谷

解法

100% L \leq 100

维护一个管理循环的栈。由于循环从里到外结束,可用栈管理循环的嵌套关系。
细节比较多,如果该循环能够计入这一次的复杂度,则满足以下条件:

  1. 之前不能有不能执行的循环
  2. 循环的终点是n

不计入复杂度但能执行的循环满足以下条件中任意的一条:

  1. 起点和终点都是常数,且起点小于等于终点
  2. 起点和终点都是n

不能被执行的循环满足以下条件中任意的一条:

  1. 起点是n
  2. 起点和终点都是常数,且起点小于终点

语法错误有3类:

  1. 变量名冲突(vis管理一下)
  2. E语句结束了不存在的循环(判断栈是否为空)
  3. 结束指令后仍有未关闭的循环(同2)

对于不能被执行的循环,打标记并使之后的循环对答案无贡献,直到它出栈。逐循环计算它对总复杂度的贡献。只要把以上细节写全了就能A。

代码

// Code by KSkun, 2017/11
#include <cstdio>
#include <cstring>
#include <stack>
#include <algorithm>

struct Loop {
    char var;
    int beg, end;
    Loop(char var, int beg, int end): var(var), beg(beg), end(end) {}
};

inline int getComp(char *str) {
    if(str[2] == 'n') {
        int res = 0, len = strlen(str);
        for(int i = 4; i < len; i++) {
            if(str[i] == ')') break;
            res = res * 10 + str[i] - '0';
        }
        return res;
    }
    return 0;
}

inline int getNum(char *str) {
    if(str[0] == 'n') return -1;
    int res = 0, len = strlen(str);
    for(int i = 0; i < len; i++) {
        res = res * 10 + str[i] - '0';
    }
    return res;
}

bool vis[50], iserr;
char op[5], var[5], beg[5], end[5], complexity[100], notValid;
int t, l, now, all, exp;
std::stack<Loop> sta;

int main() {
    //freopen("p3952.in", "r", stdin);
    scanf("%d", &t);
    while(t--) {
        memset(vis, 0, sizeof vis);
        while(!sta.empty()) sta.pop();
        iserr = false;
        notValid = '-';
        now = all = 0;
        scanf("%d%s", &l, complexity);
        exp = getComp(complexity);
        while(l--) {
            scanf("%s", op);
            if(op[0] == 'F') {
                scanf("%s%s%s", var, beg, end);
                // ERR proccess
                if(vis[var[0] - 'a']) {
                    iserr = true;
                    continue;
                } 

                // non-ERR process
                if(iserr) continue;
                vis[var[0] - 'a'] = true;
                Loop t = Loop(var[0], getNum(beg), getNum(end));
                sta.push(t);
                if(((t.beg == -1 && t.end != -1) || (t.beg != -1 && t.end != -1 && t.beg > t.end)) && notValid == '-') {
                    // if this is the first loop that any loop after it will not run. 
                    notValid = t.var;
                }
                if(notValid != '-') continue;
                if(t.end == -1 && t.beg != -1) {
                    now++;
                }
            } else if(op[0] == 'E') {
                // ERR proccess
                if(sta.empty()) {
                    // if stack is empty when trying to proccess an 'E' command
                    iserr = true;
                    continue;
                }

                // non-ERR proccess
                if(iserr) continue;
                Loop t = sta.top();
                sta.pop();
                vis[t.var - 'a'] = false;
                if(t.var == notValid) {
                    notValid = '-';
                }
                if(notValid != '-') continue;
                all = std::max(all, now); // always update all complexity value
                if(t.end == -1 && t.beg != -1) {
                    now--;
                }
            }
        }
        if(!sta.empty()) {
            // if stack is still not empty when all commands are inputed
            iserr = true;
        }
        if(iserr) {
            printf("ERR\n");
        } else if(all == exp) {
            printf("Yes\n");
        } else {
            printf("No\n");
        }
    }
    return 0;
}

总结

考场上可能是没考虑周全,思路也比较乱,导致写出来代码很乱而且只有90,没能A掉。是一个遗憾。这种大模拟,重要的就是给自己偷懒,把能复用的代码写函数,能包的包起来,并且思路清晰分模块编写就好。本以为再也不会碰到这种大模拟题了,CCF在搞啥啊orz

逛公园

一句话题意

给一个有向图,求从1到N点长度不超过最短路径+K的路径数量。

解法

30% K = 0

最短路计数。(模板题:洛谷P1144洛谷P1608
在SPFA求最短路的时候加一个cnt数据记录到某点最短路的数量。如果一个点的最短路被更新,cnt重置为1,否则如果找到另外一条最短路,对cnt加一。

100% 1 \leq N \leq 100000, 1 \leq M \leq 200000, 0 \leq K \leq 50

很容易设计一个DP状态为dp[u][k]表示u点比最短路长k的路径数量,而初始dp[1][0]为1。规定dis数组为1到每个点的最短路距离,对于u点的所有入边(入边长度为len)的起点v,转移如下
dp[u][k] = \sum dp[v][dis[u]-dis[v]+k-len]
如果用SPFA转移,要考虑转移的状态是否已经计算过。我们规定转移的顺序是优先转移dis小的点,因此我们要保持转移队列的优先级有序。
但如果这个转移可以从N点开始,在反图上转移就不存在上面的问题。这种方案我们可以采用记忆化搜索。这道题时限有3s,能够满足需要。
那么0边如何处理?记忆化搜索不需要管0边,但是如果搜索的过程中遇到了一个0环,那么这个环任意跑多少圈都是一条可取的路径,这种情况是无解的,因此要在搜索过程中记录下搜索状态(u, k)是否已经在搜索栈中,可以用vis数组来处理一下。如果在栈中,就说明存在0环。
另外有一个很玄学的问题,搜索的最后要加一行dfs(N, K + 1);,否则就会有数据跑不过去,例如这一组数据

2 2 0 50
1 2 0
2 1 0

答案应该为-1,但是如果不加那一行答案就是1了。然而洛谷的数据好像不加那一行也能过,我不是很清楚原理是啥。

代码

// Code by KSkun, 2018/1
#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>

struct io {
    char buf[1 << 26], *s;

    io() {
        fread(s = buf, 1, 1 << 26, stdin);
    }

    inline int read() {
        register int res = 0, neg = 1;
        while(*s < '0' || *s > '9') {
            if(*s == '-') neg = -1;
            s++; 
        } 
        while(*s >= '0' && *s <= '9') res = res * 10 + *s++ - '0';
        return res * neg;
    }
};

io ip;

#define read ip.read 

struct Edge {
    int to, w;
    Edge(int to, int w): to(to), w(w) {}
};

std::vector<Edge> vec[100005], rev[100005];

inline void addedge(int u, int v, int w) {
    vec[u].push_back(Edge(v, w));
    rev[v].push_back(Edge(u, w));
} 

int T, N, M, K, P, ut, vt, wt, dis[100005], dp[100005][55], ans;
bool vis[100005][55], zcircle;

std::queue<int> que;
bool inque[100005];

inline void spfa(int st) {
    memset(dis, 0x3f, sizeof dis);
    memset(inque, 0, sizeof inque);
    que.push(st);
    dis[st] = 0;
    inque[st] = true;
    while(!que.empty()) {
        int u = que.front();
        que.pop();
        inque[u] = false;
        for(int i = 0; i < vec[u].size(); i++) {
            int v = vec[u][i].to;
            if(dis[v] > dis[u] + vec[u][i].w) {
                dis[v] = dis[u] + vec[u][i].w;
                if(!inque[v]) {
                    inque[v] = true;
                    que.push(v);
                }
            } 
        } 
    }
}

inline int dfs(int u, int k) {
    //printf("u %d k %d\n", u, k);
    if(vis[u][k]) {
        zcircle = true;
        return -1;
    }
    if(dp[u][k] != -1) return dp[u][k];
    vis[u][k] = true;
    dp[u][k] = 0;
    for(int i = 0; i < rev[u].size(); i++) {
        int v = rev[u][i].to;
        int delta = dis[u] - dis[v] + k - rev[u][i].w;
        //printf("  v %d delta %d\n", v, delta);
        if(delta < 0) continue;
        dp[u][k] = (0ll + dp[u][k] + dfs(v, delta)) % P;
        if(zcircle) return -1;
    }
    vis[u][k] = false;
    return dp[u][k];
}

int main() {
    T = read();
    while(T--) {
        memset(dp, -1, sizeof dp);
        memset(vis, 0, sizeof vis);
        zcircle = false;
        ans = 0;
        N = read();
        M = read();
        K = read();
        P = read();
        for(int i = 1; i <= N; i++) {
            vec[i].clear();
            rev[i].clear();
        }
        for(int i = 0; i < M; i++) {
            ut = read();
            vt = read();
            wt = read();
            addedge(ut, vt, wt);
        }
        spfa(1);
        dp[1][0] = 1;
        for(int i = 0; i <= K; i++) {
            ans = (0ll + ans + dfs(N, i)) % P;
        }
        dfs(N, K + 1);
        if(zcircle) {
            printf("-1\n");
        } else {
            printf("%d\n", ans);
        }
    }
    return 0;
}

总结

图论+DP的写法是第一次碰到,最短路计数的骗分也不会写,NOIP之前的我果然是个鶸。

NOIP2017 提高组 Day2

吐槽

第一次碰到纯粹的数据结构题,第一次碰到这么难的DP。

个人成绩

100,20,30

题目列表(评测来自洛谷)

赛题 #A: P3958 奶酪 | BFS,并查集
赛题 #B: P3959 宝藏 | 状压DP,记忆化搜索
赛题 #C: P3960 列队 | 数据结构,线段树,平衡树

奶酪

一句话题意

给一堆球体,有两个平行于xOy坐标平面的平面作为底面(z=0)和顶面(z=h),两个相交或者相切的球体可以视作联通,求是否能找到一条路径从底面到顶面去。

解法

100% 1 \leq N \leq 1000

建图BFS或者并查集检查是否有底面-顶面球体对在同一集合内都可以解决这个问题。题目本身并不难,而且默认了z坐标为正也不会爆long long。

代码

// Code by KSkun, 2018/1
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#include <algorithm>

struct Ball {
    int x, y, z;
    Ball(int x, int y, int z): x(x), y(y), z(z) {}
};

std::vector<Ball> balls;
std::vector<int> vec[1005];
std::queue<int> que;
bool top[1005], success, vis[1005];
int T, n, h, r, x, y, z;

inline long long dis(int a, int b) {
    long long x = 1ll * (balls[a].x - balls[b].x) * (balls[a].x - balls[b].x),
        y = 1ll * (balls[a].y - balls[b].y) * (balls[a].y - balls[b].y),
        z = 1ll * (balls[a].z - balls[b].z) * (balls[a].z - balls[b].z);
    return x + y + z;
}

inline bool can(int a, int b) {
    return dis(a, b) <= 1ll * r * r * 4;
}

int main() {
    scanf("%d", &T);
    while(T--) {
        success = false;
        while(!que.empty()) que.pop();
        memset(top, 0, sizeof top);
        memset(vis, 0, sizeof vis);
        balls.clear();
        scanf("%d%d%d", &n, &h, &r);
        for(int i = 1; i <= n; i++) {
            vec[i].clear();
        }
        for(int i = 1; i <= n; i++) {
            scanf("%d%d%d", &x, &y, &z);
            balls.push_back(Ball(x, y, z));
            if(std::abs(h - z) <= r) top[i] = true;
            if(std::abs(z) <= r) que.push(i);
            for(int j = 0; j < balls.size() - 1; j++) {
                if(can(i - 1, j)) {
                    vec[i].push_back(j + 1);
                    vec[j + 1].push_back(i);
                }
            }
        }
        // BFS
        while(!que.empty()) {
            int u = que.front();
            que.pop();
            if(top[u]) {
                success = true;
                break;
            }
            vis[u] = true;
            for(int i = 0; i < vec[u].size(); i++) {
                int v = vec[u][i];
                if(!vis[v]) {
                    que.push(v);
                }
            }
        } 
        if(success) {
            printf("Yes\n");
        } else {
            printf("No\n");
        }
    }
    return 0;
} 

感想

当时写的时候还在纠结是不是要爆ll,然后开了ull。后来发现我多虑了。好多人都栽在这个题上。我当时写的大常数BFS是能在老爷机上A了的,所以并不清楚发生了什么。

宝藏

一句话题意

给一个无向图,让你找一个最小生成树,生成树的每条边产生Z \times W的代价,其中Z是该边起点到指定根的距离(每条边长度为1),W是边本来的权值。

解法

100% 1 \leq n \leq 12, 0 \leq m \leq 1000

考虑使用dis数组记录每个点到指定根的距离,然后设计状压DP,dp[S]表示已经加入生成树的点为S的时候的最佳方案。
转移直接使用搜索即可。对于集合S中的每一个点尝试转移到它们的下层点。如果发现下层点的状态方案不是最优,则更新最优并搜索下去,否则回溯。
然后枚举指定根,每一次都搜一遍。
代码相当短,题目本身也并不是很难,就是一个大暴力。

代码

// Code by KSkun, 2018/1
#include <cstdio>
#include <cstring>
#include <algorithm>

const int INF = 0x3f3f3f3f;

int n, m, gra[15][15], dis[15], dp[1 << 13], ans = INF, ut, vt, wt;

inline int dfs(int s) {
    for(int i = 1; i <= n; i++) {
        // if the point has been chosen
        if(s & (1 << (i - 1))) {
            for(int j = 1; j <= n; j++) {
                // if the goal is reachable and hasn't been chosen
                if(!(s & (1 << (j - 1))) && gra[i][j] != INF) {
                    // if there is a better solution
                    if(dp[s | 1 << (j - 1)] > dp[s] + dis[i] * gra[i][j]) {
                        int lst = dis[j];
                        dis[j] = dis[i] + 1;
                        dp[s | 1 << (j - 1)] = dp[s] + dis[i] * gra[i][j];
                        dfs(s | 1 << (j - 1));
                        dis[j] = lst;
                    }
                }
            }
        }
    }
}

int main() {
    memset(gra, 0x3f, sizeof gra);
    memset(dis, 0x3f, sizeof dis);
    scanf("%d%d", &n, &m);
    for(int i = 0; i < m; i++) {
        scanf("%d%d%d", &ut, &vt, &wt);
        gra[ut][vt] = gra[vt][ut] = std::min(gra[ut][vt], wt);
    }
    // choose who is the root
    for(int i = 1; i <= n; i++) {
        memset(dis, 0x3f, sizeof dis);
        memset(dp, 0x3f, sizeof dp);
        dis[i] = 1;
        dp[1 << (i - 1)] = 0;
        dfs(1 << (i - 1));
        ans = std::min(ans, dp[(1 << n) - 1]);
    }
    printf("%d", ans);
    return 0;
}

感想

考场上在想为什么今天还没出DP题以及这题怎么看怎么像生成树,然后疯狂想办法证贪心,然后GG。考完试一个华一大佬告诉我就是个贪心生成树,然后他也GG了,233。

列队

一句话题意

有一个n \times m的矩阵,每次从(x, y)拿出一个数并且删掉它,然后左对齐、上对齐,再把拿出的这个数插入(n, m)位置。给定很多(x, y),每次求拿出的那个数是什么。

解法

30% n = 1

其实我们发现我们可以用一个长为m+q的线段树来维护这个序列。当每一次删点的时候记录哪些区间的子树里删了这个点,就可以统计出一个该区间内总共删掉的点数,然后每次查找第几个元素的时候以此为参考到它的左子树or右子树查找。至于多出来的那些个可能乱序的元素,就直接扔进vector然后如果要找就算一下下标。

100% 1 \leq n, m, q \leq 3 \times 10^5

对30%的做法进行延伸,我们可以发现,这个题需要n+1棵线段树,对应n行的前m-1个元素和最后一列,每一次在该行中删除一个元素,然后在最后一列中删一个元素加入该行,再往最后一列中加入一个元素。这样一来,思路就很明确了。

代码

// Code by KSkun, 2018/1
#include <cstdio>
#include <algorithm>
#include <vector>

struct io {
    char buf[1 << 26], *s;

    io() {
        //freopen("p3960-13.in", "r", stdin);
        //freopen("p3960-13.out", "w", stdout);
        fread(s = buf, 1, 1 << 26, stdin);
    }

    inline int read() {
        register int res = 0, neg = 1;
        while(*s < '0' || *s > '9') {
            if(*s == '-') neg = -1;
            s++; 
        } 
        while(*s >= '0' && *s <= '9') res = res * 10 + *s++ - '0';
        return res * neg;
    }
};

io ip;

#define read ip.read 

int n, m, q, x, y, N;
int lc[10000005], rc[10000005], del[10000005], root[300005], tot = 0;
std::vector<long long> vec[300005];

inline long long query(int o, int l, int r, int rnk) {
    if(l == r) return l;
    // res stands for how many points left subtree has
    int mid = (l + r) >> 1, res = mid - l + 1 - del[lc[o]];
    if(res >= rnk) return query(lc[o], l, mid, rnk);
    else return query(rc[o], mid + 1, r, rnk - res);
}

inline void update(int &o, int l, int r, int rnk) {
    if(!o) o = ++tot;
    del[o]++;
    if(l == r) return;
    int mid = (l + r) >> 1;
    if(mid >= rnk) update(lc[o], l, mid, rnk);
    else update(rc[o], mid + 1, r, rnk);
}

inline long long del_line(int x, int y) {
    long long res = query(root[x], 1, N, y);
    update(root[x], 1, N, res);
    return res < m ? 1ll * (x - 1) * m + res : vec[x][res - m];
}

inline long long del_row(int x) {
    long long res = query(root[n + 1], 1, N, x);
    update(root[n + 1], 1, N, res);
    return res <= n ? 1ll * res * m : vec[n + 1][res - n - 1];
}

int main() {
    n = read();
    m = read();
    q = read();
    N = std::max(n, m) + q;
    while(q--) {
        x = read();
        y = read();
        if(y != m) {
            long long res = del_line(x, y);
            vec[n + 1].push_back(res);
            printf("%lld\n", res);
            vec[x].push_back(del_row(x));
        } else {
            long long res = del_row(x);
            vec[n + 1].push_back(res);
            printf("%lld\n", res);
        }
    }
    return 0;
}

感想

从来没写过纯粹的数据结构题的我在考场上就是懵逼的qwq

密码保护:浴谷八连测总结

密码保护:浴谷八连测总结

无法提供摘要。这是一篇受保护的文章。

模运算性质、快速幂(取模)

模运算性质、快速幂(取模)

模运算的一些性质

同余公式

形如a≡b (mod d)的式子被称为同余公式,因为此式中a与b模d后的值相等,故被称为同余公式。相关性质将基于它展开。

它等价于(a-b)|d、a-(a/d)*d=b、a=b+nd (n∈Z)。

运算性质

  1. a≡a(mod d)
  2. 对称性 a≡b (mod d)→b≡a (mod d)
  3. 传递性 (a≡b (mod d),b≡c (mod d))→a≡c (mod d)
  4. 如果a≡x (mod d),b≡m (mod d),则
    a+b≡x+m (mod d)
    a-b≡x-m (mod d)
    a*b≡x*m (mod d )
    a/b≡x/m (mod d)
  5. a≡b (mod d)则a-b|d
  6. a≡b (mod d)则an≡bn (mod d)
  7. 如果ac≡bc (mod m),且c和m互质,则a≡b (mod m)

运算规则

(a + b)  mod  p = (a  mod  p + b  mod  p)  mod  p
(a – b)  mod  p = (a  mod  p – b  mod  p)  mod  p
(a * b)  mod  p = (a  mod  p * b  mod  p)  mod  p
ab  mod  p = ((a mod p)b)  mod  p
结合率: ((a + b)  mod  p + c)  mod  p = (a + (b + c)  mod  p)  mod  p
((a * b)  mod  p * c) mod  p = (a * (b * c)  mod  p)  mod  p
交换率: (a + b)  mod  p = (b+a)  mod  p
(a * b)  mod  p = (b * a)  mod  p
分配率: ((a + b) mod  p * c)  mod  p = ((a * c)  mod  p + (b * c)  mod  p)  mod  p
重要定理:若a≡b (mod  p),则对于任意的c,都有(a + c) ≡ (b + c) (mod p)
若a≡b (mod  p),则对于任意的c,都有(a * c) ≡ (b * c) (mod p)
若a≡b (mod  p),则对于任意的c,都有ac≡ bc (mod p)

快速幂(取模)

简介

快速幂是运用分治思想降低朴素算法复杂度的算法,是OI中大数据常用的优化小trick。

说明

假设我们现在正在解决求a^b的问题,其中b非常大,而且通常会遇到朴素算法运算中,中间得数就已经会爆int甚至爆long long的情况。

我们知道a^b=(a^(b/2))^2,因此我们可以将求a^b问题转化为求a^(b/2)问题,如此递归下去,总会遇到b/2=0的时候,此时递归就到头了。这样做的时间复杂度是O(logn)的(而朴素是O(n)的)。

我们还可以运用取模运算的性质(a * b)  mod  p = (a  mod  p * b  mod  p)  mod  p来对每一步的值取模缩小值的范围。此算法便是边幂边取模的算法。

如果b是一个单数怎么办?也不要紧:a^b=(a^(b/2))^2*a即可解决。

代码

下面是递归式快速幂。

int fpow(int x, int p) { // 求x^p 
    if(p == 0) return 1;
    int t = fpow(x, p / 2);
    if(p % 2 == 0) {
        return t * t;
    } else {
        return t * t * x;
    }
}

int fpow_m(int x, int p, int m) { // 求(x^p)%m
    if(p == 0) return 1;
    int t = fpow_m(x, p / 2, m) % m;
    if(p % 2 == 0) {
        return (t * t) % m;
    } else {
        return ((t * t) % m * (x % m)) % m; 
    }
}

下面是非递归式快速幂。

inline LL fpow(LL x, LL k) {
    LL t = 1;
    while(k) {
        if(k & 1) t = (t * x) % MO;
        x = (x * x) % MO;
        k >>= 1;
    }
    return t;
}

应用

应用不用说了,常用于数据大的NOIP提高组复赛T1之类的题目。

参考资料

模运算与同余公式的性质 – 根号下的麻辣烫 – CSDN博客

例题:2011 北大OpenJudge百练4010

描述

已知长度最大为200位的正整数n,请求出2011^n的后四位。

输入

第一行为一个正整数k,代表有k组数据,k<=200接下来的k行,

每行都有一个正整数n,n的位数<=200

输出

每一个n的结果为一个整数占一行,若不足4位,去除高位多余的0

样例输入

3
5
28
792

样例输出

1051
81
5521

分析

快速幂是肯定得用的,但是同时也考验了高精度幂运算的知识。

此处我用了一个小trick,打表知取2011的k次幂时,只要整数k后三位相等,他们运算后所得数后四位相等。故省去了写高精度的功夫。

代码

#include <cstdio>
#include <cstring>
#include <cstdlib> 

int fpow_m(int x, int p, int m) { // 求(x^p)%m
    if(p == 0) return 1;
    int t = fpow_m(x, p / 2, m) % m;
    if(p % 2 == 0) {
        return (t * t) % m;
    } else {
        return ((t * t) % m * (x % m)) % m; 
    }
}

int n, t;
char s[205];
char tmp[5];

int main() {
    scanf("%d", &n);
    for(int i = 0; i < n; i++) {
        memset(s, 0, sizeof s);
        memset(tmp, 0, sizeof tmp);
        scanf("%s", s);
        int len = strlen(s), cnt = 0;
        for(int i = 0; i < 3; i++) {
            if(len - 3 + i < 0) continue;
            tmp[cnt] = s[len - 3 + i];
            cnt++;
        }
        //printf("p: %s\n", tmp);
        printf("%d\n", fpow_m(2011, atoi(tmp), 10000));
    } 
    return 0;
}

参考资料

快速幂+快速幂经典例题 -  - CSDN博客