AcWing Blue Bridge Cup グループ AB チュートリアル 08、数論

序文

しばらく前に、面接でいくつかのアルゴリズムに関する質問に対処できるようにするために、質問を磨く道に乗り出しました。質問のほとんどは Li Kou プラットフォームで行われています。現在のスコアは 400+ です。また、その後新しい学校に着いて、ブルーブリッジカップも開催していることを知りました。関連するプログラミングコンテスト、もう一度挑戦するつもりです、体系的にアルゴリズムを学びたいです(それまでは主にバックエンドプロジェクトでしたが、アルゴリズムについては短期間でよくわかりませんでしたが、少し前に初めて acwing プラットフォームを訪問しましたが、上記のコースは比較的体系的で、質問がたくさんあると感じましたプラットフォームなので、しばらくはacwingの路線を辿るつもりですが、一緒に頑張りましょう!

  • 現在、私は Java グループに参加する予定なので、ソリューションはすべて Java です。

すべてのブログ ファイル ディレクトリ インデックス:ブログ ディレクトリ インデックス (継続的に更新)

この章の貪欲な演習のリスト: すべてのトピックの Java ソリューションへのリンク

第8講 学習期間:2023.1.20~2023.1.27

画像-20230127135833702

例:

エクササイズ:

1. 整数論

例 1: AcWing 1246. 算術数列 (最大公約数、第 10 回蘭橋杯県大会の C++B 質問 7)

分析する

データ量は 100,000、時間計算量は O(n.logn), O(n) です。

まず、等差数列は次のようになります。

x  x+d  x+2d  x+3d ... x+n*d

タイトルに、この等差数列のセットのほんの一部だけが与えられており、最も短い等差数列の数を見つけるように求められていると述べていますが、このとき、与えられた一連の数列の数を使用する必要があります。対応する d 値を見つけます。d の値を見つけるにはどうすればよいですか?

等差数列内の各数値が固定値 x と対応する d の倍数で構成されていることがわかり、最大公約数を使用して d を見つけることを考えることができます。

例: x+3d - x = 3d、x + 3d - (x - d) = 2d を使用し、2d と 3d を行き来させて最大公約数を計算すると、d が得られます。

等差数列全体の数値を取得するにはどうすればよいでしょうか?

数量 = (最大 - 最小) / d + 1

特殊な場合、たとえば: 1 1 1、つまり d が 0 の場合、その数は n でなければなりません。

それからACに行きましょう!

問題の解決策: 最大公約数

複雑さの分析: 時間計算量 O(n.logn)、空間計算量 O(n)

import java.io.*;
import java.util.*;

class Main {
    
    
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 100010;
    static int n;
    static int[] a = new int[N];
    
    public static void main(String[] args) throws Exception{
    
    
        n = Integer.parseInt(cin.readLine());
        String[] ss = cin.readLine().split(" ");
        for (int i = 0; i < n; i++) {
    
    
            a[i] = Integer.parseInt(ss[i]);
        }
        //排序
        Arrays.sort(a, 0, n);
        //0可以作为起点来进行与之后的数字进行公约数计算
        int d = 0;
        for (int i = 0; i < n; i ++ ) {
    
    
            //a[i] - a[0]只留下对应的n * d,然后求取最大公约数
            d = gcd(d, a[i] - a[0]);
        }
        //若是最大公约数为0,直接返回整个个数
        //举例:0 0 0 0 0
        if (d == 0) {
    
    
            System.out.println(n);
        }else {
    
    
            //等差数列依次为:x  x+d  x+2d  x+3d,(最后一个-第一个) / d + 1 = 3 + 1 = 4
            System.out.println((a[n - 1] - a[0]) / d + 1);
        }
    }
    
    //计算最大公约数
    public static int gcd(int a, int b) {
    
    
        return b == 0 ? a : gcd(b, a % b);
    }
}

画像-20230120113346901

実際、ソートの目的は最小値と最大値を見つけることだけなので、O(n) を使用するだけで最適化できます。

import java.io.*;
import java.util.*;

class Main {
    
    
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 100010;
    static int n;
    static int[] a = new int[N];
    
    public static void main(String[] args) throws Exception{
    
    
        n = Integer.parseInt(cin.readLine());
        String[] ss = cin.readLine().split(" ");
        for (int i = 0; i < n; i++) {
    
    
            a[i] = Integer.parseInt(ss[i]);
        }
        //找到最小值,最大值
        int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
        for (int i = 0; i < n; i ++) {
    
    
            min = Math.min(a[i], min);
            max = Math.max(a[i], max);
        }
        int d = 0;
        for (int i = 0; i < n; i ++ ) {
    
    
            //减去最小值
            d = gcd(d, a[i] - min);
        }
        if (d == 0) {
    
    
            System.out.println(n);
        }else {
    
    
            //最大值-最小值
            System.out.println((max - min) / d + 1);
        }
    }
    
    public static int gcd(int a, int b) {
    
    
        return b == 0 ? a : gcd(b, a % b);
    }
}

画像-20230120113519570


例 2: AcWing 1295. X の因子連鎖 (算術の基本定理、オイラーの篩、複数の集合の順列)

トピックリンク: 1295. X の因子連鎖

分析する

この質問の N サイズは 100 万で、時間計算量は O(n.logn)、O(n) で制御する必要があります。

この質問は、2 つのソリューションの価値を中心に分析されます。

シーケンスの最大長を求める

ここで、算術の基本定理を学ぶ必要があります。この定理では、1 より大きい任意の自然数 N は、N が素数でない場合、N は有限数の素数の積に一意に分解できます。すべての整数は、いくつかの素因数の積の形式に一意に分解できます。

  • 積の式は次のとおりです。 N=P1 a1 P2 a2 P3 a3 * ... Pn an。ここで、P1<P2<P3...<Pn はすべて素数であり、指数 ai は正の整数です。このような分解は N の標準分解と呼ばれます。最古の証明はユークリッドによって与えられました。

この質問のタイトル説明要件によると、1 より大きい X の因数は、次の項目を割り切れる前の項目を満たす厳密に増加するシーケンスを形成します。

举例严格递增情况:2    2*2    2*2*3   2*2*3*4

このトピックの説明は、算術の基本定理と完全に一致しています: N=P1 a1 P2 a2 P3 a3 * ... Pn an 、算術の基本定理に従って数値を解決すると、次の最大長を取得できます。a1 + a2 + a3 + a4 + ... + an というシーケンス

なぜ?例えば:

180 = 2 2 * 3 2 * 5

a1 = 2,a2 = 2,a3 = 1此时能够构成序列最大长度为5
//注意:题目中意思是去让你从对应因子组成中的数进行选择,例如2*2,你可以选择2以及4加入到这个序列当中去,不是说选了一个2,就只能再选一个2
//组成序列的如下,就是5
2   2*2   2*2*3   2*2*3*3   2*2*3*3*5

算術の基本定理の公式を使用する必要があるからこそ、オイラーのふるい法を使用して 1 ~ n のすべての素数を見つけ、各数値の最小の素因数をふるい落とす必要があります(この質問のデータ量は 100 万、オイラーふるい法の複雑さは O(n) です)

オイラースクリーニング法の考え方と詳細コード(ナイーブスクリーニング、エジプシャンスクリーニング、オイラースクリーニングを含む):数論のオイラースクリーニング法(簡易スクリーニング、エジプシャンスクリーニングの詳細コードを含む)

各数値の最小の素因数を選別することは、算術の基本定理の対応する式を推定するために使用できるため、非常に重要です。たとえば、180 は、最小の素因数 (素数) が 2 であると推定できます。そして、180/4=45、45 の最小の素因数は 3、45/9=5、5 の最小の素因数は 5、最終的な構成は 180 = 2 2 * 3 2 * 5なりますこの時点で、対応する a1、a2、a3...an を取得でき、シーケンスの最大長を見つけることができます。

最大長を満たすシーケンスの数: 実際には、最大長のシーケンスの数が完全に配置され、重複が排除されます。

Y の一般的な証明定理: 最初に数値そのものではなく、数値の増分をマッピングします。元のシーケンスは a1、a2、a3...an、マッピングされたシーケンスは a1、a 2 a 1 a2\ です。a1以上1 _2 _a 3 a 2 a3\over a22 _3 _anan − 1 an\over an-11 _、これら 2 つのシーケンスは対応しており、最初のシーケンスを与えることで 2 番目のシーケンスを見つけることができ、2 番目のシーケンスのすべての数値も素因数であるため、シーケンスの数はすべての素因数の完全な配列になります (重複する番号を削除します)。

最大長のシーケンスに対して複数のスキームを実行することもできますか? 上記の証明定理を説明するために実際の例を使用してみましょう: 180 = 2 2 * 3 2 * 5

2   2*2   2*2*3   2*2*3*3   2*2*3*3*5

//实际上我们还可以从3开始,同样能够构成最大长度序列
3   3*2   3*2*2   3*2*2*3   3*2*2*3*5

したがって、実際には、列を配置するための最大長のシーケンスの数を求めています(同じ要素の完全な配置を削除します)。

対応するマルチセットの順列は次のとおりです。

N 個の要素が完全に配置されています:N!

次の例と同じ要素を削除します。

3 3 3,相互可交换的次数为2*3,实际上就是ai!,ai指的是对应3的个数

同一の要素が削除された最終的な完全な置換は次のようになります: ( a 1 + a 2 + a 3 + . . . + an ) ! a 1 ! a 2 ! . . . an ! (a1 + a2 + a3 + ... + an )!\over a1!a2!...an!1 ! _ 2 ! _ ... _( a 1 + a 2 + a 3 + ... + an ) !

解決策: 数論 - 算術の基本定理、オイラーのふるい、複数の集合の順列

複雑さの分析: 時間計算量 O(n)、空間計算量 O(n)

import java.util.*;
import java.io.*;

class Main {
    
    
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    //2的20次方为1,048,576
    static final int N = 1100010;
    //存储合数
    static int[] coms = new int[N];
    //存储质数
    static int[] primes = new int[N];
    //存储质数的数量
    static int primeCount = 0;
    //存储所有数的最小质因子
    static int[] minPrimes = new int[N];
    static int x;
    
    //欧拉筛选
    public static void getPrimes(int n) {
    
    
        //遍历所有的数字
        for (int i = 2; i <= n; i++) {
    
    
            if (coms[i] == 0) {
    
    
                primes[primeCount++] = i;
                //存储质数的最小质因子,当前就是本身
                minPrimes[i] = i;
            }
            //遍历质数数组
            for (int j = 0; primes[j] * i <= n && j < primeCount; j++ ) {
    
    
                int t = primes[j] * i;//合数值
                //建立合数
                coms[t] = 1;
                //构建合数的最小质因子
                minPrimes[t] = primes[j];
                //若是当前i能够整除primes数组中的质数,此时直接提前结束
                if (i % primes[j] == 0) break;
            }
        }
    }
    
    public static void main(String[] args) throws Exception{
    
    
        //提前进行欧拉筛选
        getPrimes(N - 1);
        
        //来进行多轮操作
        while (true) {
    
    
            //读取两个数组
            String line = cin.readLine();
            if (line == null || line.length() == 0) break;
            //获取到数字
            int n = Integer.parseInt(line);
            //构建n = ...,基本的算数定理公式
            //使用fact、sum来分别记录N的质因子以及质因子的个数
            int[] fact = new int[100], sum = new int[100]; 
            //记录当前n的质因子的个数,对应fact的下标
            int k = 0;
            //记录质因子总数(题解1:序列的最大长度)
            int total = 0;
            //尝试去分解n的质因数
            while (n > 1) {
    
    
                //获取到当前n的最小质因子
                int minPrime = minPrimes[n];
                //设置第k个质因子为当前n的最小质因子
                fact[k] = minPrime;
                //对应第k个质因子数量从0开始计算
                sum[k] = 0;
                //为false情况:一旦不能够整除,此时就该要去找后一个不同的质因子
                while (n % minPrime == 0) {
    
    
                    //当前质因子数量+1
                    sum[k]++;
                    //质因子总数+1
                    total++;
                    n /= minPrime;
                }
                k++;
            }
            
            //开始计算(题解2:满足最大长度的序列的个数)
            long res = 1;
            //首先计算质因子总数的全排列方案数
            for (int i = 1; i <= total; i++) {
    
    
                res *= i;
            }
            //接着去除重复的情况
            //遍历所有的质因子
            for (int i = 0; i < k; i ++) {
    
    
                //每个质因子的阶乘(使用res继续除以)
                for (int j = 1; j <= sum[i]; j++) {
    
    
                    res /= j;
                }
            }
            
            //输出
            System.out.printf("%d %s\n", total, String.valueOf(res));
        }
        
    }
}

画像-20230120180033030


例 3: AcWing 1296. スマート ステファニー

本質: 暴力的な探索 + 剪定

研究記事: AcWing 1296. Smart Stefanie—詳細なソリューションAcWing 1296. Smart Stefanie (蘭橋杯 C++ AB グループ チュートリアル)—ビデオ

分析する

まず第一に、問題の意味を理解しましょう。それは、数値の約数の合計を与え、この条件を満たす複数の数値 (複数ある場合があります) を見つけさせることです。

  • 例: 約数の合計は 42 で、数値 20、26、および 41 の制約の合計は 42 です。
  • 数字 20 の約数は 1 2 4 5 10 20 で、その合計は 42 です。

この問題における暴力列挙の考え方は、数値 S に対して 1 から S-1 までのすべての数の約数を列挙し、その約数の合計が S に等しいかどうかを判断することです。ただしSの最大値は20億なので必ずタイムアウトします。

この時点で、約数の合計の公式を使用できます: S = (1+p1+p1 2 +…+p1 a1 )(1+p2+p2 2 +…+p2 a2 )…(1+pn+pn 2 +… +pn an )

上の式を満たす約数 s がある場合、3x5x9x17x33x65x129 = 635037975 になります。これは、O(n) の複雑度に比較的近い、つまり、この質問の S の最大値は 20 億です。

このプロセスでは、dfs を使用して検索し、2 層の for ループを使用して列挙します。

for(p : 2,3,5,7,...)
    for(a : 1,2,3,...)
        if(S mod (1+p1+p1^2+...+p1^a1) == 0)
            dfs(下一层)
//dfs(质数下标开始位置,上一层的实际答案结果,s剩余待整除的值)

以下は、約数の合計が 42 である 3 つの結果値 20、26、および 41 を示します。約数の合計の式を使用して取得された結果の値を確認できます。

①20:1 2 4 5 10 20

  • 20 = 2 2 * 5 = (2 0 + 2 1 + 2 2 )*(5 0 +5 1 )=42

②26:1 2 13 26

  • 26 = 2 * 13 = (2 0 + 2 1 ) * (13 0 + 13 1 ) = 42

③41:1 41

  • 41 = 41 = (41 0 + 41 1 ) = 42

枝刈りを使用して計算を減らすにはどうすればよいですか?

  • 実際の式は実際には () * () で構成されていることがわかります。そのため、実際の境界範囲は sqrt(s) になる可能性があります。この場合、S = (1 + pi) という特殊なケースを考慮する必要があります。
  • たとえば、約数の合計が 42 の場合、境界は sqrt(s) であるため、この時点で通過する最大の素数は 7 で、1 つの結果の数値は 41 になります。この場合、境界に到達できない場合、次のようになります。 S = (1 + pi) の pi が素数かどうかを考慮する必要があるだけです。

さらに、この質問は素数をスクリーニングする必要があるため、O(n) の複雑さを達成するためにオイラー スクリーニングを使用する必要があります。単純なスクリーニング、エラ スクリーニング、およびオイラー スクリーニングのアイデアとコードについては、ブログを参照してください: オイラー スクリーニング方法数論 (詳細については、簡単なスクリーニング、オングストローム フィルター コードを含む)

問題の解決: オイラーふるい + 約数の合計 (dfs) + 枝刈り

sqrt() を使用しない DFS コード: 20 億の配列スペースを開く必要があり、この質問は解決できません

public static void dfs(int last, int pro, int s) {
    
    
    if (s == 1) {
    
    
        res[len++] = pro;
        return;
    }
    //你可以看到这里primes[i] <= s对于这种情况也依旧是可以实现的,但是对于本题S为20亿则会直接超时
    //优化点:对于primes[i] <= s / primes[i]则可直接对(s - 1)来判断进行优化。
    for (int i = last + 1; primes[i] <= s; i++) {
    
    
        int p = primes[i];
        for (int j = 1 + p; j <= s; p *= primes[i], j += p) {
    
    
            if (s % j == 0) {
    
    
                dfs (i, pro * p, s / j);
            }
        }
    }
}

枝刈りの最適化を追加: sqrt(s) を走査、s-1 状況の判定条件を追加

public static void dfs(int last, int pro, int s) {
    
    
    if (s == 1) {
    
    
        res[len++] = pro;
        return;
    }
    //剪枝(优化):提前判断当前(s-1)是否是一个质数,若是(s-1)>上一个质数 && (s-1)是一个质数(主要也是解决数据量过大的问题)
    //对应下方for循环遍历的终点是:primes[i] <= s / primes[i]
    //举例:对于值为41,其约数为1、41,走下面的for循环(若是原本primes[i] <= s / primes[i])时,实际上只会遍历到最大质数为7就无法往后了,所以这边来进行提前剪枝操作
    int pre = last >= 0 ? primes[last] : 1;
    if (s - 1 > pre && isPrime(s - 1)) {
    
    
        res[len++] = pro * (s - 1);
    }
    for (int i = last + 1; primes[i] <= s / primes[i]; i++) {
    
    
        int p = primes[i];
        for (int j = 1 + p; j <= s; p *= primes[i], j += p) {
    
    
            if (s % j == 0) {
    
    
                dfs (i, pro * p, s / j);
            }
        }
    }
}

注: Java を使用する場合、System.out.print を直接使用するとタイムアウトします。BufferedReader と PrintWriter の使用を推奨します。

完全なコード:

import java.util.*;
import java.io.*;

class Main {
    
    
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final PrintWriter out = new PrintWriter(new BufferedOutputStream(System.out));
    static final int N = 500000;//特判S = 1+p,最大S = p*p
    
    //欧拉筛所需要数组
    //flag表示合数数组,true为合数
    static boolean[] flag = new boolean[N];
    //存储质数
    static int[] primes = new int[N];
    static int cnt = 0;
    
    //存储每一组数据的答案
    static int[] res = new int[N];
    static int len = 0;
    
    //欧拉筛
    public static void getPrimes(int n) {
    
    
        //遍历所有情况
        for (int i = 2; i <= n; i++) {
    
    
            if (!flag[i]) primes[cnt++] = i;
            //枚举所有primes数组中的情况来提前构造合数
            for (int j = 0; j < cnt && primes[j] * i <= n; j ++) {
    
    
                int pre = primes[j] * i;
                flag[pre] = true;
                if (i % primes[j] == 0) break;
            }
        }
    }
    
    //dfs进行暴搜
    //last:表示上一个用的质数的下标(primes数组的下标)是什么
    //pro:当前计算res答案是多少
    //s: 表示每次处理一个()后还有剩余的值
    public static void dfs(int last, int pro, int s) {
    
    
        //表示当前已经把s凑出来了,记录答案
        if (s == 1) {
    
    
            res[len++] = pro;
            return;
        }
        //剪枝:提前判断当前(s-1)是否是一个质数,若是(s-1)>上一个质数 && (s-1)是一个质数
        //直接来进行计算res结果值
        int pre = last >= 0 ? primes[last] : 1;
        if (s - 1 > pre && isPrime(s - 1)) {
    
    
            res[len++] = pro * (s - 1);
        }
        //枚举所有以i作为下标的质数,实际就是N公式中的pi
        for (int i = last + 1; primes[i] <= s / primes[i]; i++) {
    
    
            int p = primes[i];
            //j指的是枚举()中的各种情况,例如i = 2,此时枚举情况为(1 + 2)、(1 + 2 + 2*2)、(1 + 2*2 + 2*2*2)
            for (int j = 1 + p; j <= s; p *= primes[i], j += p) {
    
    
                //当前能够整除情况则进入下一个层 
                if (s % j == 0) {
    
    
                    //下一层从primes下标为[i + 1]的开始(因为for循环是从last+1开始的),当前括号*之前的值 = pro * p,若是j = (1 + 2 + 2*2),此时
                    //p就是2*2=4,这个p实际上就是N公式里的一个2平方
                    //目标约数和为s,到了下一层其剩余和即为s / j
                    dfs (i, pro * p, s / j);
                }
            }
        }
    }
    
    //判断是否是质数(由于之前primes数组仅仅开了sqrt(20亿)也就只有50万,所以这里需要进行遍历一遍质数数组来进行判断校验)
    public static boolean isPrime(int x) {
    
    
        //若是x在50万范围,直接从flag数组中判断返回即可
        if (x < N) return !flag[x];
        //若是>=50万,那么就进行遍历质数数组看是否有能够整除的,如果有那么直接返回
        for (int i = 0; primes[i] <= x / primes[i]; i++) {
    
    
            if (x % primes[i] == 0) return false;
        }
        return true;
    }
    
    public static void main(String[] args) throws Exception{
    
    
        //欧拉筛
        getPrimes(N - 1);
        String line = cin.readLine();
        //读取数据
        while (line != null && line.length() > 0) {
    
    
            //目标约数之和为s
            int s = Integer.parseInt(line);
            dfs(-1, 1, s);
            out.println(len);
            if (len != 0) {
    
    
                //对结果进行排序
                Arrays.sort(res, 0, len);
                //输出
                for (int i = 0; i < len; i++) {
    
    
                    out.print(res[i] + " ");
                }
                out.println();
                len = 0;//重置结果数组的长度为0
            }
            out.flush();
            line = cin.readLine();
        }
    }
}

画像-20230124131816932


例 4: AcWing 1299. Wuzhishan (拡張 Euclid)

分析する

全長をn、宙返りの距離をd、初期位置をx、目標位置をyとし、目標点に到達できない場合はImpossible、到達可能な場合は最小回数を出力します。宙返りが出力されます。

この問題の 2 に対応する式は、x + bd = y (mod n) です。これは、点 x から開始し、b 回の宙返りの距離を加算し、最終的に円全体の距離を乗じた目標点としての位置を取得することを意味します。

次に、x + bd = y (mod n) = y + an、つまり点 y に円 a の長さを加えたものを変換します。

x + bd = y + an は、次の式を変換します。x、d、y、n は既知であり、-an + bd = y - x に変換されます。

画像-20230124173448404

注: 現時点では、この式は拡張ユークリッドの公式 gcd(a, b) = d に対応しており、対応する方程式 ax + by = d が成り立ちます。これは、拡張ユークリッドを使用することで逆転できます。 a と b を導入します。この質問の式。

このとき、この問題の考え方は、拡張ユークリッド アルゴリズムを使用して gcd(n, d) = gcd を求め、yx が割り切れるかどうかを判断して最大公約数を求めます。割り切れない場合は、 「不可能」と出力し、割り切れる場合は、宙返りの最小数を計算します。

yx で得られる gcd が割り切れる場合に、解が存在しないと判断できるのはなぜですか?

  • -an + bd は最大公約数を求めるので、求めた最大公約数で y - x を割り切れない場合は解がなく、出力できません。

最小宙返り回数を計算するにはどうすればよいですか?

拡張ユークリッド計算を行ったり来たりする場合、実際に計算される式は次のとおりです: -an + bd = gcd(n, d)。タイトルについては、正しくするために -an + bd = y - x にする必要があります。式 子には yx もあります。このとき、両側に (y - x) / gcd(n, d) を掛ける必要があります。このとき、b は最終的な b の結果値 -an + bd = y - x!

このとき、b の値は -an + bd = y - x の ab 解の集合のみであり、すべての ab 解は拡張ユークリッド公式を使用して解の集合を通じて取得できます。の最小 A b 解!

現在のトピックである an + bd = y - x に対応して、b の最小値を取得するには、まず次の 2 つの式を取得します。

  • b = b0 - kn'
  • n' = n / (y - x)

このとき、b の現在の解はすべて b = b0 - k.(n / d) です。ここで、n と d は定数です。使用する必要がある b の最小値については、b0 % ( n / (y - x)) つまり、この b0 は実際には取得できますが、k を決定することはできません。そのため、取得した結果の値 b を直接 mod に使用できます。つまり、最終的な最小値は b = b mod (n /(y - x))。

また、b mod (n / (y - x)) で負の数を避けるために、n = n / (y - x) を設定し、(b mod n + n) % n で負の数を正の数に変換できるためです。数字!

解決策: 拡張 Euclid

複雑さの分析: 時間計算量 O(logn)、空間計算量 O(1)

import java.io.*;
import java.util.*;

class Main {
    
    
    
    static final Scanner cin = new Scanner(System.in);
    
    static int t;
    static long n, d, x, y;
    
    //扩展欧几里得
    public static long exGcd(long a, long b, long[] arr) {
    
    
        if (b == 0) {
    
    
            arr[0] = 1;
            arr[1] = 0;
            return a;
        }
        //递归求得最大公约数
        long gcd = exGcd(b, a % b, arr);
        //反向推导求得一组x,y解: x = y',y = x' - a/b*y'
        long temp = arr[0];
        arr[0] = arr[1];
        arr[1] = temp - a / b * arr[1];
        return gcd;
    }
    
    public static void main(String[] args) {
    
    
        int t = cin.nextInt();
        while (t != 0) {
    
    
            //读取数据
            n = cin.nextLong();
            d = cin.nextLong();
            x = cin.nextLong();
            y = cin.nextLong();
            long[] arr = new long[2];
            //获取到最大公约数和一组xy解
            long gcd = exGcd(n, d, arr);
            //得到一组x与y解(这里用a与b表示)
            long a = arr[0];
            long b = arr[1];
            //若是y - x能够整除gcd(n, d)那么此时就说明有解
            if ((y - x) % gcd != 0) {
    
    
                System.out.println("Impossible");
            }else {
    
    
                //ax + by = gcd(a, b)  转换为  ax + by = y - x,所以两边需要乘上(y - x) / gcd(a, b)
                b *= (y - x) / gcd;
                //接着需要进行计算最小值:b = b0 - kn’、n’  = n / (y - x)
                //由于上面式子转换仅仅只是b变量进行了转换,所以n依旧使用原先的gcd进行转换
                n /= gcd;
                //避免b % n为负数情况
                System.out.println((b % n + n) % n);
            }
            
            t--;
        }
    }
}

画像-20230124202624548

エクササイズ

問題 1: AcWing 1223。最大スケール (ミディアム、ブルーブリッジカップ)

分析する

トピックリンク: AcWing 1223. 最大比率

この問題は等比数列を与えるもので、その比は一定ですが、この質問では等比数列の一部のみが与えられるため、等比数列の中で最大の等比比を見つけることができます。

データ量は多くなく等比数列の数値は10個のみですが、最大値が比較的大きいためlong型を使用する必要があります。

完全な元の算術シーケンスが次のとおりであるとします。 a, a * pq \frac{p}{q}qp, a * ( pq \frac{p}{q}qp) 2、 a * ( pq \frac{p}{q}qp) 3、 a * ( pq \frac{p}{q}qp) 4 , … , a * ( pq \frac{p}{q}qp) n-1

そのうちのいくつかを抽出しましょう: b1、b2、b3、b4、すると各数値の構成は ( pq \frac{p}{q}qp) k、 b[i] のグループごとに b[i - 1] の比を取得できます。つまり、pkqk \frac{p^k}{q^k}qkpp kと q k in 、これら 2 つの上限値と下限値は、最大公約数を見つけることで取得できます。

ここで本題に戻り、( pq \frac{p}{q}qp) kの最大値は、実際には指数の最大公約数を見つけるためのものであり、指数の最大公約数については、ローリングと減算の方法を使用する必要があります。知識ポイントは、ローリングとローリングで確認できます。ローリングと減算

導出はgcd(x,y) = gcd(y,x%y) = gcd(y,x−y)ローリングと減算によって実行されます。 f(p x ,p y ) = p gcd(x,y) = p gcd(y,x−y) = f(p y ,p( x−y )) = f ( p y , pxpy \frac{px}{py}ぴー_エックス_)、つまり、次のように求めることができます

p xと p yのべき乗の最大公約数べき乗 p gcd(x,y)を求めます

指数関数でローリング除算法が使用できないのはなぜですか? 次の例を参照してください。この質問の減算方法は、指数を減算することです。

  • ローリングと除算を使用します: gcd(5 2 , 5 3 ) = 5 2
  • ローリング減算を使用します: gcd_sub(5 2 , 5 3 ) = 5 1

問題解決方法:転がり減算法(多位相減算法)

複雑さの分析: 時間計算量 O(n.logn)、ソート計算量、空間計算量 O(n)

import java.util.*;
import java.io.*;

class Main {
    
    
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 110;
    static int n;
    static long[] x = new long[N], p = new long[N], q = new long[N];
    
    
    //最大公约数(辗转相除)
    public static long gcd(long a, long b) {
    
    
        return b == 0 ? a : gcd(b, a % b);
    }
    
    //辗转相减,求得指数的最小公约数
    public static long gcd_sub(long a, long b) {
    
    
        if (a < b) {
    
    
            long temp = b;
            b = a;
            a = temp;
        }
        if (b == 1) return a;
        return gcd_sub(b, a / b);
    }
    
    public static void main(String[] args) throws Exception{
    
    
        n = Integer.parseInt(cin.readLine());
        String[] ss = cin.readLine().split(" ");
        for (int i = 0; i < n; i ++ ) {
    
    
            x[i] = Long.parseLong(ss[i]);
            //System.out.println(x[i]);
        }
        //对所有数字进行排序
        Arrays.sort(x, 0, n);
        
        
        //记录p与q数组成对的数量
        int cnt = 0;
        
        //查询所有数字乘数p/q的p与q的值
        for (int i = 1; i < n; i ++ ) {
    
    
            if (x[i] != x[i - 1]) {
    
    
                //获取到两个数的最大公约数
                long gcd = gcd(x[i], x[0]);
                //利用最大公约数来计算得到分数中的p与q
                p[cnt] = x[i] / gcd;
                q[cnt++] = x[0] / gcd;
            }
        }
        
        //开始计算所有(p/q)^n最大公约数
        long P = p[0];
        long Q = q[0];
        for (int i = 1; i < cnt; i ++ ) {
    
    
            P = gcd_sub(P, p[i]);
            Q = gcd_sub(Q, q[i]);
        }
        System.out.println(P + "/" + Q);
    }
}

画像-20230126161706453


演習 2: Acwing 1301. C ループ (単純な拡張ユークリッド)

トピックリンク: Acwing 1301. C ループ

分析する

k ビット システムは、すべての変数が k ビットのみを格納できることを意味します。したがって、毎回の +c の値は、実際には mod 2 kの値になります

この時点で、方程式をリストできます: (A + xC) mod 2 k = B。ここで、A、C、B は固定値であり、mod 2 k は実際には y.2 kに置き換えることができます

この時点で (A + xC) mod 2 k = B は A + xC - y.2 に変換されます。k = B は xC - y.2 に変換されます。k = B - A に変換されます。

画像-20230126163228402赤丸で囲ったものは定数です。

この時点で、ユークリッドの拡張: xa + yb = d を考えることができます。これを逆にすると、x と y の解のセットを取得でき、すべての解は解のセットから取得できます。

対応する式は次のとおりです。

画像-20230126163710124

実際、最終的にこの質問の式は次のようになります。

x' = x / (B - A)
y' = y / (B - A)

x = x0 + ky'
y = y0 + kx'

常時循環可能かどうかは、B - A が gcd(a, b) を変更できるかどうかだけで判断でき、最終的に必要となるのは循環回数 x = x0 です。 %y'。

解決策: 拡張 Euclid

複雑さの分析: 時間計算量 O(logn)、空間計算量 O(1)

import java.util.*;
import java.io.*;

class Main {
    
    
    
    static final Scanner cin = new Scanner(System.in);
    static long A, B, C, K;
    
    //扩展欧几里得
    public static long exGcd(long a, long b, long[] arr) {
    
    
        if (b == 0) {
    
    
            arr[0] = 1;
            arr[1] = 0;
            return a;
        }
        long d = exGcd(b, a % b, arr);
        //通过公式去化解转为 x = y',y = x‘ - a/b*y'
        long temp = arr[0];
        arr[0] = arr[1];
        arr[1] = temp - a / b * arr[1];
        return d;
    }
    
    public static void main(String[] args) throws Exception{
    
    
        while(!isStop()) {
    
    
            //若是A==B,则输出0
            if (A == B) {
    
    
                System.out.println(0);
                continue;
            }
            // x.C - y.2^k = B - A
            //计算C与2^k的最大公约数
            long[] arr = new long[2];
            //提前定义好 x.C + y.2^k = gcd(C, 2^k)  =>  用a替代为C,b替代为2^k
            long a = C;
            long b = 1L << K;//注意,这个b变量必须使用1L,表示long类型,否则有误
            long gcd = exGcd(a, b, arr);
            if ((B - A) % gcd != 0) {
    
    
                System.out.println("FOREVER");
            }else {
    
    
                long x = arr[0];
                long y = arr[1];
                //将 x.a + y.b = gcd(a, b) 转为  x.a - y.b = B - A
                //此时只需要将这个x去进行一个转换
                x *= (B - A) / gcd;
                
                //若是想要取得一个最小运行次数x
                //y' = y / gcd
                b = b / gcd;
                
                //取得最小整数  x = x0 % b
                System.out.println((x % b + b) % b);
            }
        }
    }
    
    public static boolean isStop() throws Exception{
    
    
        A = cin.nextLong();
        B = cin.nextLong();
        C = cin.nextLong();
        K = cin.nextLong();
        return A == 0 && B == 0 && C == 0 && K == 0;
    }
}

画像-20230126172306946


2.DFS

エクササイズ

演習 1: AcWing 1225。通常の問題 (ミディアム、DFS、スタック)

分析する

まず、質問の意味を理解するために、|、()、x の合計 4 つの記号があり、いくつか例を示します。

xx|xxx    =>  xxx     //|选择左右两边最多的一组
(xx|xxx)x   => xxxx   //()与左右两边进行相连接

トピックの指定範囲は 100 であり、合法であることが保証されています。

スタック シミュレーションのアイデア:

遇到(、x、|符号直接入栈
遇到)开始进行匹配规则:
	循环出栈直到出现(,过程中可能会有|来进行计数判断选择最大的个数,最终来进行出栈(

dfs のアイデア: 文字列全体をツリーとして想像し、文字列に対して左から右に dfs を実行します。

  • ① 見つかったら(一致するまで 1 階層下に再帰的に)終了します。
  • ② | に遭遇した場合、|xxx の右側で dfs() 再帰を実行し、取得した長さと現在の長さの間の最大値を取得します。
  • ③遭遇した場合)に直接ブレイクして終了となります。
  • ④xに遭遇したらこの時点でres+1を行う。

画像-20230126195812088

解決策 1: スタック シミュレーション

複雑さの分析: 時間計算量 O(n)、空間計算量 O(n)

import java.util.Scanner;
import java.util.Stack;

class Main {
    
    

    static final Scanner cin = new Scanner(System.in);
    static Stack<Character> s = new Stack<>();
    
    //假设碰到)时来进行的计数操作
    public static void count() {
    
    
        //碰到)
        int cnt = 0;
        int c = 0;
        while (!s.isEmpty() && s.peek() == 'x') {
    
    
            c++;
            s.pop();
            cnt = Math.max(cnt, c);
            //如果说碰到了|,重新计数
            if (!s.isEmpty() && s.peek() == '|') {
    
    
                c = 0;
                s.pop();
            }
        }
        if (!s.isEmpty() && s.peek() == '(') {
    
    
            //此时碰到(
            s.pop(); 
        }
        //入栈cnt个x
        for (int i = 1; i <= cnt; i++) {
    
    
            s.push('x');
        }
    }

    public static void main(String[] args) {
    
    
        String line = cin.next();
        for (char ch: line.toCharArray()) {
    
    
            if (ch == '(' || ch == '|' || ch == 'x') {
    
    
                s.push(ch);
            }else {
    
    
                count();
            }
        }
        //结束之后再计算下,可能会出现情况:xx|xxxxx
        count();
        System.out.println(s.size());

    }
}

画像-20230126200119570

解決策 2: dfs

複雑さの分析: 時間計算量 O(n)、空間計算量 O(n)

import java.util.*;
import java.io.*;

class Main {
    
    
    
    static final Scanner cin = new Scanner(System.in);
    static char[] arr;
    static int k;
    
    //计数
    public static int dfs() {
    
    
        int res = 0;
        while (k < arr.length) {
    
    
            //匹配(.....)
            if (arr[k] == '(') {
    
    
                k++;
                res += dfs();
                k++;
            }else if (arr[k] == ')') {
    
     //(的结束递归
                break;
            }else if (arr[k] == '|') {
    
      //比较左右最大数量 ...|...
                k++;
                res = Math.max(res, dfs());
            }else {
    
    
                //若是碰到x
                k++;
                res++;
            }
        }
        return res;
    }
    
    public static void main(String[] args) {
    
    
        arr = cin.next().toCharArray();
        int res = dfs();
        System.out.println(res);
        cin.close();
    }
    
}

画像-20230126200114538

演習 2: AcWing 1243. キャンディ (状態圧縮 + IDA* および dp 状態圧縮、青いブリッジ カップ)

トピックリンク: AcWing 1243. キャンディ

分析する

アイデア 1: 状態圧縮 + IDA* (dfs)

カバレッジの重複の問題については、IDA* の使用を検討してください。

  • 反復カバレッジ問題: 行列が与えられ、すべての列がカバーされるように最小数の行を選択します。

IDA* の場合は、次の 3 つの部分を考慮する必要があります。

  1. 反復的に深化する: レイヤーごとに判断して、完全にカバーできるかどうかを確認します。
  2. 最も少ない列を選択する: 検索するケースをできるだけ少なく選択します。
  3. 実現可能性の枝刈り: 評価関数 h(state) を使用して、状態 state に少なくとも必要な行数を示します。現在の検索行数と一致する場合は下に進み、一致しない場合は事前にプルーニングを終了します。

完全なアイデア全体は、非常に詳細なコード コメントで確認できます。

ここでは、バイナリ操作を実行するための状態圧縮の手順をいくつか投稿します。

画像-20230127134622111

アイデア 2: 状態圧縮 dp

状態表現:f[i][j]前の i 個のキャンディー パッケージに対して選択された最小キャンディー パッケージの数を示し、状態は j です。

初期化: f[i][0]=0、その他はデフォルトの最大値。

状態の計算:f[i][j] = min(f[i][j], f[i][j & ~c[i]] + 1)

  • j & ~c[i]ターゲット状態のバイナリ値が c[i] と現在の状態のバイナリ値 j にマージされることを示します。

解決策 1: IDA*(dfs)

複雑さの分析: 時間計算量 O(b^d)。ここで、b は分岐係数、d は最初の解の深さです。空間複雑度 O(d)

import java.util.*;
import java.io.*;

class Main {
    
    
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 110, K = 22;
    //n表示行数,m表示糖果种类数量,k表示每袋有几个糖果
    static int n, m, k;
    //candy表示每袋糖果的二进制表示
    //log2表示根据二进制位去映射对应的糖果品类,下标是二进制位,值就是糖果品类编号
    //若是key:0001也就是1,value就是糖果品类编号0
    //若是key: 0010也就是2,value就是糖果品类编号1
    //糖果品类有M种,默认在初始化时进行编号0 - M-1
    static int[] candy = new int[N], log2 = new int[1 << K];
    //存储key为糖果类型编号,value为糖果包装有该糖果编号的二进制值
    static Map<Integer, List<Integer>> map = new HashMap<>();
    
    
    public static void main(String[] args) throws Exception{
    
    
        String[] ss = cin.readLine().split(" ");
        n = Integer.parseInt(ss[0]);
        m = Integer.parseInt(ss[1]);
        k = Integer.parseInt(ss[2]);
        //初始化糖果品类编号对应二进制位的映射
        //log2[1] = 0,log2[2] = 1,log2[4] = 2 ... 
        for (int i = 0; i < m; i ++ ) {
    
    
            log2[1 << i] = i;
        }
        //读取每袋糖果
        for (int i = 0; i < n; i ++ ) {
    
    
            ss = cin.readLine().split(" ");
            //对每袋糖果中的多个品类来进行状态压缩到一个二进制curCandy
            int curCandy = 0;
            for (int j = 0; j < k; j ++ ) {
    
    
                int candyType = Integer.parseInt(ss[j]);//读取到糖果编号
                //curCandy更新当前的糖果袋子具有的糖果种类,例如二进制形式curCandy = 00000,candyType = 1,而之前在log2中说明糖果种类为[0,M-1]
                //所以(1 << (candyType - 1))即为00001,此时00000 | 00001 = 00001
                //同上其他情况,curCandy = 00001,candyType = 3,此时此时00001 | 00100 = 00101
                curCandy = curCandy | (1 << (candyType - 1));
            }
            
            candy[i] = curCandy;//将每袋糖果具有的糖果类别进行状态压缩后添加到candy数组中
            
            //记录指定糖果类型编号有哪些袋糖果
            for (int j = 0; j < m; j ++ ) {
    
    
                //判断当前糖果包中是否有对应编号为j的糖果
                if (((curCandy >> j) & 1) == 1) {
    
    
                    if (!map.containsKey(j)) {
    
    
                        map.put(j, new ArrayList<>());
                    }
                    List<Integer> packages = map.get(j);
                    packages.add(curCandy);
                }
            }
        }
        //若是在map中具有的糖果类型种类没有m个,那么直接结束
        if (map.size() < m) {
    
    
            System.out.println("-1");
        }else {
    
    
            //1、迭代加深
            //进行尝试递归寻找糖果包方案数量
            int count = 0;
            //数量上限为糖果的品类,若是超过上限还没有找到说明肯定没有该方案
            while (count <= m) {
    
    
                //判断当前选择count数量的糖果包是否能够集全
                if (dfs(count, 0)) {
    
    
                    break;
                }else {
    
    
                    count++;
                }
            }
            //此时得到方案数
            System.out.println(count);
        }
    }
    
    //尝试寻找方案数量
    //count:表示当前还能选择的糖果包数量
    //state:表示当前已选糖果类型的状态,若是M为5,达到11111即可表示已经选中
    public static boolean dfs(int count, int state) {
    
    
        //3、使用估价函数来判断当前状态是否能够继续往下进行
        //若是当前不能选糖果包了 或者 还可以选并且至少需要糖果包的数量>当前剩余的数量
        if (count == 0 || mustNeed(state) > count) {
    
    
            //若是m为5,则判断当前已经状态state是否为11111
            return state == (1 << m) - 1;
        }
        //2、选择尽可能少的列
        //寻找还没有凑齐的多个糖果类型(从右往左开始)中最少糖果包的那个糖果列
        int minCol = -1;
        for (int i = (1 << m) - 1 - state; i > 0; i -= lowbit(i)) {
    
    
            //获取到二进制位从右往左第一个1,也就是第一个还未选择的糖果类型
            int col = log2[lowbit(i)];
            if (minCol == -1 || map.get(minCol).size() > map.get(col).size()) {
    
    
                minCol = col;
            } 
        }
        //枚举最少数量的糖果类型列,进行递归处理
        for (int pack: map.get(minCol)) {
    
    
            //还能选择的糖果数量-1,当前已经选择糖果状态列补上当前糖果包有的糖果列
            //state为00101,pack为00010,此时state | pack即为00111
            if (dfs(count - 1, state | pack)) {
    
    
                return true;
            }
        }
        return false;
    }
    
    //当前状态最少需要的糖果包数
    //state:表示当前已选糖果类型的状态
    public static int mustNeed(int state) {
    
    
        int ans = 0;
        //(1 << m) - 1 - state:表示的是当前还未选的糖果类型二进制状态
        for (int i = (1 << m) - 1 - state; i > 0;) {
    
    
            //当前所需要的糖果类型行号
            int col = log2[lowbit(i)];
            //获取到对应糖果类型的所有糖果
            List<Integer> packages = map.get(col);
            //来将该行对应的所有糖果包都去进行消除当前i二进制状态中与糖果包共有的1
            for (int pack: packages) {
    
    
                //假设i二进制为:11111,pack为00101
                //那么i & ~pack = 11010,相当于消去该糖果包有的糖果类型
                //~pack实际上就是表示所有二进制为取反,原本pack=00100,~pack即可转为11011
                i = i & ~pack;
            }
            ans++;
        }
        return ans;
    }
    
    //从右往左得到第一个1的下标k(从0开始),返回的结果值为2^k
    //例如x的二进制位0010,此时下标k为1,返回值就是2^1 = 2
    public static int lowbit(int x) {
    
    
        return x & -x;
    }
    
}

画像-20230127123030122

解決策 2: 状態圧縮 dp

複雑さの分析: 時間計算量 O(n.2 m )、m の最大値は 20、つまり 100*104 万、約 1,000 万回の計算になります。空間の複雑さ O(2 m )

import java.io.*;
import java.util.*;

class Main {
    
    
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 110, K = 20, INF = 101;
    //c表示所有糖果包的状态压缩;
    static int[] c = new int[N];
    //f表示从前i个物品中选且状态是j的最小糖果包数量。
    static int[] f = new int[1 << K + 5];
    static int n, m, k;
    
    public static void main(String[] args) throws Exception{
    
    
        String[] ss = cin.readLine().split(" ");
        n = Integer.parseInt(ss[0]);
        m = Integer.parseInt(ss[1]);
        k = Integer.parseInt(ss[2]);
        //初始化每个糖果包的状态压缩
        for (int i = 1; i <= n; i ++ ) {
    
    
            ss = cin.readLine().split(" ");
            for (int j = 1; j <= k; j ++ ) {
    
    
                int candyType = Integer.parseInt(ss[j - 1]);
                c[i] |= 1 << (candyType - 1);
            }
        }
        //初始化状态数组
        for (int i = 1; i < 1 << m; i ++ ) f[i] = INF;
        //一种口味都没有情况最少是0包糖果
        f[0] = 0;
        //遍历所有的糖果包
        for (int i = 1; i <= n; i ++ ) {
    
    
            //遍历所有1 - 2^m-1状态(从大到小)
            for (int j = (1 << m) - 1; j >= 0; j -- ) {
    
    
                //j & ~c[i]表示当前二进制状态j去除掉c[i]状态的共有1
                f[j] = Math.min(f[j], f[j & ~c[i]] + 1);
            }
        }
        if (f[(1 << m) - 1] == INF) {
    
    
            System.out.println("-1");
        }else {
    
    
            System.out.println(f[(1 << m) - 1]);
        }
    }
}

画像-20230127134838882


参考記事

[1] 例 1 算術数列: AcWing 1246. 算術数列 - 問題の解決策 1AcWing 1246. 算術数列 - 問題の解決策 2AcWing 1246. 算術数列 (蘭橋杯 C++ AB グループ チュートリアル クラス) - 合計ビデオ説明

[2] 例 3 スマート ステファン: AcWing 1296. スマート ステファン - 詳細なソリューションAcWing 1296. スマート ステファン (蘭橋杯 C++ グループ AB チュートリアル コース) - ビデオAcWing 1296. スマート ステファン (Java)

[3]. 例 4 Wuzhishan: AcWing 1299. Wuzhishan-問題解決法AcWing 1299. Wuzhishan (Euclid X1、Y1、X2、Y2 の関係の拡張)AcWing 1299. 数論 - Euclid の拡張

[4]. 演習 1 最大比率: AcWing 1223. 最大比率 (Java バージョン)

[5]. 通常の問題: AcWing 1225. Java infix 式のアイデア + 再帰的な 2 つの解決策AcWing 1225. 通常の問題 (蘭橋杯 C++ AB グループ チュートリアル)AcWing 1225. 通常の問題 - スタックの練習

[6]. Candy:アルゴリズム - 人工知能: IDA* 検索の時間計算量AcWing 1243. Candy (IDA* / DP 詳細ノート)AcWing 1243. Candy (Java Edition)AcWing 1243. Candy–>dfs+ プルーニング + ソート最適化 + 削除最適化 + IDA* + 形状圧縮AcWing 1243. Candy (蘭橋杯 C++ AB グループ チュートリアル)

おすすめ

転載: blog.csdn.net/cl939974883/article/details/128770442