作者: KSkun

[NOI2017]整数 题解

[NOI2017]整数 题解

题目地址:洛谷:【P3822】[NOI2017]整数 – 洛谷、BZOJ:Problem 4942. — [Noi2017]整数

题目描述

P博士将他的计算任务抽象为对一个整数的操作。
具体来说,有一个整数x,一开始为0。
接下来有n个操作,每个操作都是以下两种类型中的一种:

  • 1 a b:将x加上整数a \cdot 2^b,其中a为一个整数,b为一个非负整数
  • 2 k:询问x在用二进制表示时,位权为2^k的位的值(即这一位上的1代表2^k

保证在任何时候,x \geq 0

输入输出格式

输入格式:
输入的第一行包含四个正整数n,t1,t2,t3,n的含义见题目描述,t1,t2,t3的具体含义见子任务。
接下来n行,每行给出一个操作,具体格式和含义见题目描述。
同一行输入的相邻两个元素之间,用恰好一个空格隔开。

输出格式:
对于每个询问操作,输出一行,表示该询问的答案(0或1)。对于加法操作,没有任何输出。

输入输出样例

输入样例#1:

10 3 1 2
1 100 0
1 2333 0
1 -233 0
2 5
2 7
2 15
1 5 15
2 15
1 -1 12
2 15

输出样例#1:

0
1
0
1
0

说明

n≤1000000

题解

参考资料:【bzoj4942】[Noi2017]整数 压位+线段树 – GXZlegend – 博客园
这是我省选结束后正经做的第一个近年NOI题目,下面内容会涉及到我的口胡部分分思路。
首先,我们发现其实如果|a|=1,进位是找到前面的第一个1或0,对于这之间的一整段0或1进行区间取反。于是我们可以预处理出前面的第一个1和0,用三个线段树分别维护当前数字、前面的第一个1和0的位置。单次修改O(\log n),总复杂度O(n \log n),可以通过|a|=1的部分分。
因为我们发现对于一段连续的0,它前面第一个0是该位置+1,前面第一个1是一个定值;对于一段连续的1也有类似性质。我们可以考虑用三个标记表示当前区间内的值都是该位置+1还是一个定值还是两种都有,这样就可以用线段树来维护我们要记录的信息啦。
实际上呢,我们也可以不存储这些信息,我们考虑直接用线段树查,线段树存储当前区间全0全1或者混合,然后怎么脑补一下查找方法就好了。
我们可以把a转换成二进制表示,对于每一位非0的进行如上操作,由于|a| \leq 10^9,操作的位数不会超过30位,因此我们的单次修改复杂度实际上是O(30 \log n)的,总复杂度是O(n \log n),看似能过剩下的点了,但是实际上似乎需要卡一卡常数。
我们可以想到什么卡常的办法呢?对了,压位!线段树上每个点来维护一个位置的01取值常数大,那么我们一口气存它30位!那么对于合并区间信息怎么来做呢,我们考虑记子区间的或以及子区间的与。该位置的与值如果为30个1这种情况,说明区间内全1;该位置的或值如果为0,说明区间内全0。只需要魔改我们之前的那个线段树就可以了,改动其实并不大。
实现细节见代码中的注释。

代码

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

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF 
        : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    register char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 1000005, INF = (1 << 30) - 1;

int n;

#define lch o << 1
#define rch o << 1 | 1
#define mid ((l + r) >> 1)
int valo[MAXN << 2], vala[MAXN << 2], tag[MAXN << 2]; // tag表示该区间内全部变为某个值

inline void pushdown(int o) {
    if(~tag[o]) {
        valo[lch] = vala[lch] = tag[lch] = tag[o];
        valo[rch] = vala[rch] = tag[rch] = tag[o];
        tag[o] = -1; // 以-1为无标记的标志,可能存在要求变为0的情况
    }
}

// 将区间全都修改为某个值
inline void modify(int o, int l, int r, int ll, int rr, int v) {
    if(l >= ll && r <= rr) {
        valo[o] = vala[o] = tag[o] = v;
        return;
    }
    pushdown(o);
    if(ll <= mid) modify(lch, l, mid, ll, rr, v);
    if(rr > mid) modify(rch, mid + 1, r, ll, rr, v);
    valo[o] = valo[lch] | valo[rch];
    vala[o] = vala[lch] & vala[rch];
}

// 单点加
inline void add(int o, int l, int r, int x, int v) {
    if(l == r) {
        valo[o] += v; vala[o] += v;
        return;
    }
    pushdown(o);
    if(x <= mid) add(lch, l, mid, x, v);
    else add(rch, mid + 1, r, x, v);
    valo[o] = valo[lch] | valo[rch];
    vala[o] = vala[lch] & vala[rch];
}

// 查询某一段
inline int query(int o, int l, int r, int x) {
    if(l == r) {
        return valo[o];
    }
    pushdown(o);
    if(x <= mid) return query(lch, l, mid, x);
    else return query(rch, mid + 1, r, x);
}

// 找到该段往前第一段非全1的位置,-1表示没找到
inline int queryinf(int o, int l, int r, int x) {
    if(vala[o] == INF) return -1;
    if(l == r) return l;
    pushdown(o);
    if(x <= mid) {
        int t = queryinf(lch, l, mid, x);
        return ~t ? t : queryinf(rch, mid + 1, r, x);
    } else {
        return queryinf(rch, mid + 1, r, x);
    }
}

// 找到该段往前第一段非全0的位置,-1表示没找到
inline int queryblk(int o, int l, int r, int x) {
    if(!valo[o]) return -1;
    if(l == r) return l;
    pushdown(o);
    if(x <= mid) {
        int t = queryblk(lch, l, mid, x);
        return ~t ? t : queryblk(rch, mid + 1, r, x);
    } else {
        return queryblk(rch, mid + 1, r, x);
    }
}

// 在x段加v
inline void add(int x, int v) {
    int t = query(1, 0, MAXN, x);
    if(t + v <= INF) {
        add(1, 0, MAXN, x, v);
    } else { // 处理进位
        add(1, 0, MAXN, x, v - INF - 1);
        int y = queryinf(1, 0, MAXN, x + 1);
        if(y != x + 1) modify(1, 0, MAXN, x + 1, y - 1, 0);
        add(1, 0, MAXN, y, 1);
    }
}

// 在x段减v
inline void minus(int x, int v) {
    int t = query(1, 0, MAXN, x);
    if(t - v >= 0) {
        add(1, 0, MAXN, x, -v);
    } else { // 处理进位
        add(1, 0, MAXN, x, INF + 1 - v);
        int y = queryblk(1, 0, MAXN, x + 1);
        if(y != x + 1) modify(1, 0, MAXN, x + 1, y - 1, INF);
        add(1, 0, MAXN, y, -1);
    }
}

int op, a, b;

int main() {
    n = readint(); readint(); readint(); readint();
    while(n--) {
        op = readint();
        if(op == 1) {
            a = readint(); b = readint();
            if(a > 0) {
                add(b / 30, (a << (b - b / 30 * 30)) & INF);
                add(b / 30 + 1, a >> (30 - (b - b / 30 * 30)));
            } else if(a < 0) {
                a *= -1;
                minus(b / 30, (a << (b - b / 30 * 30)) & INF);
                minus(b / 30 + 1, a >> (30 - (b - b / 30 * 30)));
            }
        } else {
            a = readint();
            printf("%d\n", query(1, 0, MAXN, a / 30) & (1 << (a - a / 30 * 30)) ? 1 : 0);
        }
    }
    return 0;
}
反演(莫比乌斯反演、二项式反演)原理及应用

反演(莫比乌斯反演、二项式反演)原理及应用

莫比乌斯函数(Möbius function)

定义

莫比乌斯函数\mu(n)定义为:
\mu(n) = \begin{cases} 1 & (n=1) \\ (-1)^k & (n = p_1p_2 \cdots p_k) \\ 0 & (p^k|n, k > 1) \end{cases}
文字描述是这样的。定义\mu(1)=1;如果n是k个互异的质因数的乘积,则\mu(n)=(-1)^k;如果n中含有一个质因数的次数超过1,则\mu(n)=0
这是一个积性函数。

性质

  1. \sum_{d|n} \mu(d) = \begin{cases} 1 & (n=1) \\ 0 & (n \neq 1) \end{cases}
  2. \sum_{d|n} \frac{\mu(d)}{d} = \frac{\varphi(n)}{n}

狄利克雷卷积(Dirichlet convolution)

定义

对于数论函数f(n)和g(n),定义其狄利克雷卷积为
(f * g)(n) = \sum_{d|n} f(d)g(\frac{n}{d})

性质

  • 交换律:f * g = g * f
  • 结合律:f * (g * h) = (f * g) * h
  • 对加法的分配律:f * (g + h) = f * g + f * h
  • 积性函数的点积和狄利克雷卷积也是积性函数

莫比乌斯反演(Möbius inversion)

假设数论函数f(n)与F(n)满足:F(n) = \sum_{d|n} f(d),则
f(n) = \sum_{d|n} \mu(d) F(\frac{n}{d})
另外一种形式:
假设数论函数f(n)与F(n)满足:F(n) = \sum_{n|d} f(d),则
f(n) = \sum_{n|d} \mu(\frac{d}{n}) F(d)

二项式反演

f(n) = \sum_{i=0}^n \mathrm{C}_n^i g(i) \Rightarrow g(n) = \sum_{i=0}^n (-1)^{n-i} \mathrm{C}_n^i f(i)

参考资料

Miller-Rabin素性测试与Pollard’s rho算法

Miller-Rabin素性测试与Pollard’s rho算法

Miller-Rabin素性测试(Miller–Rabin primality test)

我们知道,朴素的素性测试的实质是枚举因子,我们枚举2至\sqrt{n}的所有正整数,一一检验是否为因子,这样做的复杂度是O(\sqrt{n})。能不能使复杂度更低?

费马小定理

关于费马小定理的阐述参见:数学笔记:数论基础(部分) | KSkun’s Blog
我们知道了费马小定理的式子,那么它的逆定理如果成立,我们就得到了一种O(\log n)判定方法。可惜的是,逆定理并不成立。
我们选取若干a,每次检查a^{p-1} \bmod p是否为1,这种测试方法叫做费马测试。然而,如果有合数使得选取某个a时通过测试,则称该合数为以a为底的伪素数。能通过所有小于p的底数的合数p称为Carmichael数。如果这些数很大,大过了我们需要的范围,那还好。可是561就是一个很小的Carmichael数,这种测试仍然不能满足需求,我们需要加强测试。

二次探测定理

对于0 < x < p,若p是素数,则方程x^2 \equiv 1 \pmod{p}的解为x_1 = 1, x_2 = p - 1
证明:原方程可以化为 (x+1)(x-1) \equiv 0 \pmod{p},即 (x+1)(x-1) 可以整除p。因此x = \pm 1
因此对于p-1我们考虑拆成d \cdot 2^r。先计算出a^d的值,然后每次平方,检查结果是否为\pm 1,若是,则检查上一次结果是否为\pm 1。如果二次检测未通过,则非素数。这种测试就是Miller-Rabin素性测试了。我们可以知道,这样做的复杂度是O(T \log n)的,T代表测试次数。
但是仍有一些特殊合数能够通过以a为底的MR素性测试,因此我们要不断更换a多次测试。一般来说,我们可以随机几个a出来用,但是也可以预设一些a,比如2 3 7 61 24251这一组能够正确检测大多数数字。可以近似认为选择k个a测试的错误率为0.25^k

代码

本代码可以通过【P3383】【模板】线性筛素数 – 洛谷一题的较小数据。

// Code by KSkun, 2018/4
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdlib>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF 
        : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

inline LL fpow(LL n, LL k, LL p) {
    LL res = 1;
    for(; k; n = n * n % p, k >>= 1) {
        if(k & 1) res = res * n % p;
    }
    return res;
}

inline bool miller_rabin(LL x) {
    if(x == 2) return true;
    LL t = x - 1, cnt2 = 0;
    while(!(t & 1)) {
        t >>= 1; cnt2++;
    }
    for(int i = 0; i < 5; i++) {
        LL a = rand() % (x - 1) + 1, now, lst;
        now = lst = fpow(a, t, x);
        for(int j = 1; j <= cnt2; j++) {
            now = lst * lst % x;
            if(now == 1 && lst != 1 && lst != x - 1) return false; // 二次检测
            lst = now;
        }
        if(now != 1) return false;
    }
    return true;
}

LL n, m, t;

int main() {
    srand(time(NULL));
    n = readint(); m = readint();
    while(m--) {
        t = readint();
        puts(miller_rabin(t) ? "Yes" : "No");
    }
    return 0;
}

Pollard’s rho算法(Pollard’s rho algorithm)

朴素的分解质因数算法是枚举每个可能的因数,并且把它们全部除出来,这样做的复杂度是O(\sqrt{n} \log n)的。有没有更好的方法?
依赖随机化怎么样?
直接随机到一个因子?概率很小。
那……如果两个因子分别为c和d且c>d,我们让a = \frac{c+d}{2}, b = \frac{c-d}{2},则a^2 - b^2 = n,枚举a和b,并检查a^2 - n是否是一个完全平方数。概率还是很小啊。
如果我们随机两个数,作差看看是不是呢?emmm没什么差别。
那么如果把作差与n取gcd呢?虽然直接枚举出因子的概率很小,但是因子的倍数是很多的吧。这种想法是能接受的。
于是我们可以随机两个数,作差,如果与n取gcd得到了一个因子则返回它。我们有一个很棒的伪随机函数f(x) = (x^2 + k) \bmod n,其中k是常量,在一个流程中一般不变化,但是取值没有特殊规定。然而,生成的随机数具有循环。
那么我们可以存下哪些数访问过了来避免进入死循环,但是可能会太大存不下呀!于是我们可以利用Floyd判环算法,即设定一个一倍速指针(f(x)),一个两倍速指针(f(f(x))),如果进入环中,两倍速指针一定会在某一时刻追上一倍速指针,此时环肯定已经完整地走完了一圈。(然而底下的代码似乎采用的是算导的方法,即记录下2的幂次结果,判断是否相等)如果遇到环,返回一个寻找失败的信息即可。不要气馁,重新设定常量k再跑一次吧。
其实PR算法的流程就是利用随机化特性找到n的一个因子,然后分治解决。那么分治的终点应该是该数为质数已经不可约了,我们把Miller-Rabin套进去,就可以判断当前数字是否是质数了。
那么总结一下这个算法的流程。

  1. 如果这个数字已经是质数了,加入这次分解的质因数表中并结束分治。
  2. 如果非质数,利用PR找到一个因子。
  3. 将n分为p和n/p两部分,分治解决。

代码

【P1075】质因数分解 – 洛谷

// Code by KSkun, 2018/4
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdlib>

#include <vector>
#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF 
        : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

inline LL fpow(LL n, LL k, LL p) {
    LL res = 1;
    for(; k; n = n * n % p, k >>= 1) {
        if(k & 1) res = res * n % p;
    }
    return res;
}

LL T, n;

// Miller-Rabin素性检测
inline bool miller_rabin(LL x) {
    if(x == 2) return true;
    LL t = x - 1, cnt2 = 0;
    while(!(t & 1)) {
        t >>= 1; cnt2++;
    }
    for(int i = 0; i < 10; i++) {
        LL a = rand() % (x - 1) + 1, now, lst;
        now = lst = fpow(a, t, x);
        for(int j = 1; j <= cnt2; j++) {
            now = lst * lst % x;
            if(now == 1 && lst != 1 && lst != x - 1) return false;
            lst = now;
        }
        if(now != 1) return false;
    }
    return true;
}

std::vector<LL> divi;

inline LL gcd(LL a, LL b) {
    if(!b) return a;
    return gcd(b, a % b);
}

inline LL randint(LL lim) {
    return (1ll * rand() * RAND_MAX + rand()) % lim + 1;
}

inline LL pollard_rho(LL x, LL k) {
    LL a = randint(x - 1), b = a, t1 = 1, t2 = 2;
    for(;;) {
        t1++;
        a = (a * a % x + k) % x;
        LL g = gcd(abs(b - a), x);
        if(g > 1 && g < x) return g;
        if(b == a) break;
        if(t1 == t2) {
            b = a; // 记录2的幂次生成的结果
            t2 <<= 1;
        }
    }
    return x;
}

inline void caldivisor(LL x, LL k) {
    if(x == 1) return;
    if(miller_rabin(x)) {
        divi.push_back(x);
        return;
    }
    LL p = x;
    while(p >= x) p = pollard_rho(x, k--);
    caldivisor(p, k);
    caldivisor(x / p, k);
}

int main() {
    srand(time(NULL));
    n = readint();
    caldivisor(n, 200);
    printf("%lld\n", *std::max_element(divi.begin(), divi.end()));
    return 0;
}

1811 — Prime Test

吐槽:

  1. 被卡了std::abs的常
  2. 数据太大,long long也会爆,得换成慢速乘
  3. ctime头在POJ上会RE
// Code by KSkun, 2018/4
#include <cstdio>
#include <cstring>
//#include <ctime>
#include <cstdlib>

typedef long long LL;

inline LL min(LL x, LL y) {
    return x < y ? x : y;
}

inline LL abs(LL x) {
    return x < 0 ? -x : x;
}

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF 
        : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

inline LL mul(LL a, LL b, LL p) {
    a %= p; b %= p;
    LL res = 0;
    while(b) {
        if(b & 1) {
            res += a; res %= p;
        }
        a <<= 1;
        if(a >= p) a %= p;
        b >>= 1;
    }
    return res;
}

inline LL fpow(LL n, LL k, LL p) {
    LL res = 1;
    for(; k; n = mul(n, n, p), k >>= 1) {
        if(k & 1) res = mul(res, n, p);
    }
    return res;
}

LL T, n;

inline bool miller_rabin(LL x) {
    if(x == 2) return true;
    LL t = x - 1, cnt2 = 0;
    while(!(t & 1)) {
        t >>= 1; cnt2++;
    }
    for(int i = 0; i < 20; i++) {
        LL a = rand() % (x - 1) + 1, now, lst;
        now = lst = fpow(a, t, x);
        for(int j = 1; j <= cnt2; j++) {
            now = mul(lst, lst, x);
            if(now == 1 && lst != 1 && lst != x - 1) return false;
            lst = now;
        }
        if(now != 1) return false;
    }
    return true;
}

LL mn;

inline LL gcd(LL a, LL b) {
    if(a == 0) return 1;
    while(b) {
        LL t = a % b; a = b; b = t;
    }
    return a;
}

inline LL pollard_rho(LL x, LL k) {
    LL a = rand() % x, b = a, t1 = 1, t2 = 2;
    for(;;) {
        t1++;
        a = (mul(a, a, x) + k) % x;
        LL g = gcd(abs(b - a), x);
        if(g > 1 && g < x) return g;
        if(b == a) return x;
        if(t1 == t2) {
            b = a;
            t2 <<= 1;
        }
    }
}

inline void caldivisor(LL x) {
    if(x == 1) return;
    if(miller_rabin(x)) {
        mn = min(mn, x);
        return;
    }
    LL p = x;
    while(p >= x) p = pollard_rho(x, rand() % (x - 1) + 1);
    caldivisor(p);
    caldivisor(x / p);
}

int main() {
    //srand(time(NULL));
    T = readint();
    while(T--) {
        n = readint();
        if(miller_rabin(n)) {
            puts("Prime");
            continue;
        }
        mn = 1e15;
        caldivisor(n);
        printf("%lld\n", mn);
    }
    return 0;
}

参考资料