ALGO-989:测试(Happy 2006)

这个玩意我写了足足两周,捏麻麻地。呜呜我好菜。

开头先骂蓝桥杯,捏麻麻地题目给的输入格式和你测试用的输入格式完全不同,啥意思啊?真正的输入格式如下:
第一行输入一个整数n,n的范围未知,但是int可以承接。随后有n组测试用例,一组测试用例一行,一行里面分两个数m和k,用空格隔开。例如:

3
2006 3
114514 1919
810 114

输出倒是一样的。然后时间限制的话其实应该不是3s,而是1s,我试了下那些网上说AC的慢方法,然后蓝桥杯这里没过。也就是说,必须得用(二分+容斥)的方法来更好的节约时间。我们会一步步地讲,毕竟我老菜了,记忆力差还不怎么懂,如果写得太简略了我回头看的时候会看不懂的。
我们大概可以分两种方法来做。我是打算都要讲的,这样能更好理解,顺便充实一下知识面。第一种方法的话,最好先学一下欧拉函数,然后根据这个规律来找到规律。第二种方法就是二分查找+容斥原理筛选非互斥数,从而找到互斥数了。我们先来讲第一种方法。我们会阐述:

  1. 积性函数的定义
  2. 欧拉函数的定义
  3. 欧拉函数的性质
  4. 解题

随后,我们在第二种方法里面,我们会阐述:

  1. 二分的思想
  2. 容斥原理的运用

这里,积性函数和乘性函数是混用的,我懒得改了(,你自己看的时候统一一下。
那我们开始吧。


1.1 积性函数的定义

首先,定义在所有正整数上的函数叫做算术函数。而如果对于任意两个任意互素的正整数m和n而言,存在有这么一个函数\(f(x)\),满足\(f(mn) = f(m)f(n)\),那么这个\(f(x)\)就叫做积性函数。而如果对于两个任意的正整数,都有\(f(mn) = f(m)f(n)\),那么这个\(f(x)\)就称之为完全乘性函数

1.2 欧拉函数的定义

欧拉函数\(\phi\)(Euler's totient function)表示的是小于等于n中,和n互质的数的个数。 ——OI Wiki

1.3 欧拉函数的性质

欧拉函数是乘性函数。这也就意味着我们可以导出这么一条性质:
\(\phi(2x) = \phi(2)f\phi(x) = 2\phi(x)\)
证明的话请参阅资料,我也不会(

1.4 解题

我们可以证明,\(gcd(a, b) = gcd(b * i + a, b)\),其中\(i \in N\),所以我们的一种做法就是,首先打表找到所有小于等于m的数,随后根据\(\phi(2x) = \phi(2)f\phi(x) = 2\phi(x)\)\(gcd(a, b) = gcd(b * i + a, b)\),想办法构造出目标的数。具体代码如下:

#include <iostream>
using namespace std;
unsigned int prime[1000000] = {};
unsigned int gcd(unsigned int a, unsigned int b)
{
    //return b == 0 ? a : gcd(b, a % b);
    while(b != 0){
        unsigned int temp = a;
        a = b;
        b = temp % b;
    }
    return a;
}
unsigned int refresh_prime(unsigned int m, unsigned int times = 1)
{
    int size = 0;
    for (int i = 1; i < m * times; i++)
    {
        if (gcd(i, m) == 1)
        {
            prime[size++] = i;
        }
    }
    return size;
}
unsigned int gen_kth_relative_prime(unsigned int m, unsigned int k, int size)
{
    int index = (k % size == 0) ? size - 1 : k % size - 1;
    int times = (k % size == 0) ? k / size - 1 : k / size;
    return times * m + prime[index];
}
int main()
{
    int n = 0, m = 0, k = 0;
    cin >> n;
    while (n--)
    {
        cin >> m >> k;
        int size = refresh_prime(m);
        cout << gen_kth_relative_prime(m, k, size) << endl;
    }
    return 0;
}

2.1 二分的思想

我觉得这个真的不用再说了吧(
本质上呢,由于元素的有序性,不管是升序还是降序,我们充分利用这种有序性,能在\(logn\)的时间内完成对一个元素的查找。这里为什么可以这样呢?因为我们发现,第i个和m互斥的数,一定小于第i+1个和m互斥的数。其实题目也说了升序排列。也就是说,我们完全可以利用这个性质,来在一个足够大的范围里面完成对特定元素的搜索。有些文章说右端点是无穷,我这里持怀疑态度,我只能说右端点充分大。有的文章又说左端点是k,我只能说可以,但是这么做的理由很不直观——第i个和m互斥的数一定大于等于m——我更喜欢从1或者2开始找。

2.2 容斥原理

我们想要查找和m互斥的数,我们当前的数是a,我们不一定知道a是不是和m互斥。首先,我想知道a和m是否互斥,其次,如果互斥,我想知道a在我们与m互斥的数的升序序列中的序号。第一个号做,第二个有点难做。这里,我们就要借助容斥原理了。
容斥原理是什么不再多言,我只说这里的应用。
m的素因子有一组,我们称这个数组为arr,那么我们找到他们全部的子集,那么根据子集的长度,我们增减我们的结果。这里这个找子集的方法可以看我之前的二进制枚举。这样就差不多了。
以下是代码:

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
vector<int> gen_prime_factor(long long n)
{
    long long temp = n;
    vector<int> result;
    for (long long i = 2; i < sqrt(n) + 1; i++)
    {
        if (temp % i == 0)
        {
            while (temp % i == 0)
            {
                temp /= i;
            }
            result.push_back(i);
        }
    }
    if(temp != 1)
    {
        result.push_back(temp);
    }
    return result;
}
long long gen_co_prime(long long target, long long m, const vector<int> prime_factor)
{
    if (m == 1 || target == 1)
    {
        return target;
    }
    long long size = (1 << prime_factor.size()) - 1;
    long long result = 0;
    for (int i = size; i > 0; i = (i - 1) & size)
    {
        long long p = 1;
        long long count = 0;
        for (int j = 0; j < prime_factor.size(); j++)
        {
            if ((i >> j) & 1)
            {
                p *= prime_factor[j];
                count++;
            }
        }

        if (count % 2 == 0)
        {
            result -= (target / p);
        }
        else
        {
            result += (target / p);
        }
    }
    return target - result;
}
long long count_mid(long long m, long long k)
{
    long long left = 1;
    long long right = 0x3fffffffffffffff;
    vector<int> prime_factor = gen_prime_factor(m);
    while (right > left)
    {
        long long mid = (right + left) / 2;
        if (gen_co_prime(mid, m, prime_factor) >= k)
        {
            right = mid;
        }
        else
        {
            left = mid + 1;
        }
    }
    return left;
}
int main()
{
    int n = 0;
    cin >> n;
    while(n--)
    {
        int m, k;
        cin >> m >> k;
        cout << count_mid(m, k) << endl;
    }
    return 0;
}
posted @ 2022-03-23 12:55  Lemon-GPU  阅读(78)  评论(0编辑  收藏  举报