考点集训

97 阅读33分钟

稀疏图

好的,我们来详细解释一下“稀疏图”这个概念。

核心定义

稀疏图 指的是图中边的数量 |E| 远小于其可能的最大边数的图。

换句话说,在一个有 |V| 个顶点的图中,可能的最大边数(对于无向图)是大约 |V|² 的数量级(精确值是 |V|*(|V|-1)/2 )。如果实际边数 |E| 远远小于这个最大值,我们就称这个图为稀疏图。

与之相对的是稠密图,即边的数量 |E| 接近可能的最大边数的图。


一个更量化的理解

虽然没有一个绝对统一、严格的数学定义(例如“边数少于 |V| log |V| 就是稀疏图”),但在算法和数据结构领域,通常用以下方式来理解和界定:

如果一个图的边数 |E| 与顶点数 |V| 的数量级相当,即 |E| ≈ O(|V|),那么这个图通常被认为是稀疏的。

例如:

  • 一个包含 10,000 个顶点和 20,000 条边的图是稀疏的(|E| ≈ 2|V|)。
  • 一个包含 10,000 个顶点和 5,000,000 条边的图(最大边数约50,000,000)也是相对稀疏的,但已经比较“稠密”了。

而如果一个图的 |E| ≈ O(|V|²),那么它就是稠密的。 例如:

  • 一个包含 1,000 个顶点和 500,000 条边的图可以被认为是稠密的(|E| ≈ |V|² / 2)。

为什么稀疏图的概念很重要?

区分稀疏图和稠密图非常重要,因为它直接影响了我们选择何种数据结构来存储图,以及选择何种算法来解决问题

1. 数据结构的选择
  • 邻接矩阵:使用一个 |V| x |V| 的二维数组来存储边。检查两个顶点是否相连非常快(O(1)),但空间复杂度为 O(|V|²)。

    • 适用于稠密图,因为空间被有效利用了。
    • 不适用于稀疏图,因为会浪费大量空间来存储不存在的边(无数个0)。
  • 邻接表:为每个顶点创建一个链表,存储与之相邻的所有顶点。空间复杂度为 O(|V| + |E|)。

    • 非常适用于稀疏图,因为它只存储实际存在的边,极大地节省了空间。
    • 对于稠密图,它也可以工作,但可能不如邻接矩阵高效。

结论:处理稀疏图时,邻接表通常是更优的选择。

2. 算法的选择

某些算法在稀疏图上的表现远好于在稠密图上的表现,反之亦然。

  • 迪杰斯特拉算法:用于求单源最短路径。

    • 其标准版本使用邻接矩阵,时间复杂度为 O(|V|²)。
    • 如果配合邻接表最小堆(优先队列) 进行优化,时间复杂度可降为 O(|E| log |V|)。
    • 对于稀疏图(|E| ≈ |V|),优化后的版本(O(|V| log |V|))比标准版本(O(|V|²))快得多。
  • 普里姆算法:用于求最小生成树。

    • 同样,使用邻接矩阵的时间复杂度是 O(|V|²)。
    • 使用邻接表和优先队列优化后,时间复杂度为 O(|E| log |V|)。
    • 因此,在稀疏图中,优化后的普里姆算法效率更高。

相反,有些算法在稠密图上更高效,例如弗洛伊德算法(所有顶点对最短路径),它的时间复杂度是固定的 O(|V|³),与边数无关。对于稀疏图,有更好的替代算法(如运行 |V| 次迪杰斯特拉算法)。


现实世界中的例子

绝大多数现实世界的网络都是稀疏图:

  • 社交网络:虽然Facebook有数十亿用户,但每个用户平均只有几百个朋友。边的数量是 O(|V|) 级别,而不是 O(|V|²) 级别。
  • 交通网络:一个城市的道路交叉点(顶点)很多,但每个交叉点通常只连接到3-4个其他交叉点。
  • 互联网:网页(顶点)通过超链接(边)连接,但每个页面的外链数是有限的。

总结

特性稀疏图稠密图
边数E<<V²,通常E≈ O(V)E≈ O(V²)
数据结构邻接表(节省空间)邻接矩阵(访问快)
算法示例优先队列优化的迪杰斯特拉、普里姆标准的迪杰斯特拉、弗洛伊德
现实世界非常常见(社交网络、交通网)相对少见(某些电路设计、完全图)

简单来说,当你遇到的问题涉及到的图边数不多,远远达不到每个顶点都和其他所有顶点相连的程度时,你首先就应该想到它是一个稀疏图,并优先考虑使用邻接表和为此优化的算法。

所以CSP2023第三题可以把m和n看做是相等的,接下来只要分析每个选项时间复杂度最大的一个

#哈夫曼树的一些误解 这个说法是完全错误的。哈夫曼树的构造过程和图的深度优先搜索(DFS)是两种完全不同的算法,它们解决的是完全不同的问题,之间没有直接联系。

这是一个常见的概念混淆。下面我将详细解释两者的区别,并分别说明它们的目的。


1. 哈夫曼树 (Huffman Tree)

  • 目的数据压缩。用于构造一种最优的前缀编码(哈夫曼编码),使得编码后的总长度最短。
  • 要解决的问题:如何用不等长的二进制码来表示一组字符(或符号),并且要保证任何一个字符的编码都不是另一个字符编码的前缀(前缀码),从而避免二义性。同时,出现频率高的字符用短码,出现频率低的字符用长码,从而实现整体压缩。
  • 构造过程(简述)
    1. 给定一组字符及其对应的权重(通常是频率)。
    2. 将这些字符每个都看作一棵只有根节点的树,组成一个森林。
    3. 每次从森林中选出权重最小的两棵树。
    4. 将它们合并成一棵新树,新树的根节点权重为两棵子树权重之和。
    5. 将新树放回森林中。
    6. 重复步骤3-5,直到森林中只剩下一棵树。这棵树就是哈夫曼树
  • 本质:一种贪心算法,用于构建最优二叉树

2. 深度优先搜索 (DFS - Depth-First Search)

  • 目的遍历或搜索图或树结构。用于探索图中的所有顶点,或者寻找从一个顶点到另一个顶点的路径。
  • 要解决的问题:如何系统地访问图中的每一个顶点,且每个顶点只访问一次。DFS的策略是“一条路走到黑”,尽可能深入地探索分支,直到无法继续前进再回溯。
  • 过程(简述)
    1. 从图中某个起始顶点 v 开始,将其标记为“已访问”。
    2. 查找 v 的第一个未被访问的邻接顶点 w
    3. 如果 w 存在,则从 w 开始继续进行DFS(递归)。
    4. 如果 w 不存在,则回溯到 v 的上一个顶点。
    5. 重复此过程,直到所有从起始顶点可达的顶点都被访问到。
  • 本质:一种基础的图遍历算法

对比表格

特性哈夫曼树 (Huffman Tree)深度优先搜索 (DFS)
应用领域数据压缩(如ZIP, JPEG文件)图/树遍历、寻路、拓扑排序等
输入一组带权重的符号一个图(或树)和一个起点
输出一棵最优编码树(哈夫曼树)图中顶点的访问序列(一种遍历顺序)
核心操作合并权重最小的树递归地访问和回溯
数据结构优先队列(最小堆)用于高效选取最小权重的树(递归调用栈或显式栈)
目标最小化编码后的总长度访问图中的所有顶点

结论

您可能将两个概念记混了。请记住:

  • 提到压缩、编码、频率,应该想到哈夫曼树
  • 提到遍历、路径、连通性,应该想到DFS(深度优先搜索)BFS(广度优先搜索)

它们之间没有构造或实现上的关系。

再讲一下散列表!!!!!

csp2023第14题

首先f(n)函数是在求十六进制下,各位数字之和,ni = f(ni-1),要求调用自身永远返回的 都是9,再反复调用结果依然是9,那么 直接能得到结果为9的是: 108,117,126,135,144,153,162,171,180 重复两次能得到9的是: 189,198 先得到27再得到9的不存在,因为19F和1A0各位数字之和都小于27

csp2023第15题

好的,我们来分析您提供的 quick_power 函数(快速幂)的时间复杂度。

代码分析

double quick_power(double x, unsigned n){
    if (n == 0) return 1;
    if (n == 1) return x;
    return quick_power(x, n/2) * quick_power(x, n/2) * ((n & 1) ? x : 1);
}

时间复杂度:O(n)

这是一个常见的错误实现,其时间复杂度是线性的 O(n),而不是预期的对数时间 O(log n)。


为什么是 O(n)?

关键在于这一行:

return quick_power(x, n/2) * quick_power(x, n/2) * ((n & 1) ? x : 1);
                    ^^^^                ^^^^
                (第一次递归调用)     (第二次递归调用)

问题在于: 您对 quick_power(x, n/2) 进行了两次独立的递归调用

让我们画出当 n = 8 时的递归树来理解:

                    pow(x, 8)
                    /        \
            pow(x, 4)        pow(x, 4)   // 计算了两次相同的pow(x,4)
            /      \          /      \
      pow(x,2)  pow(x,2)  pow(x,2)  pow(x,2) // 计算了四次相同的pow(x,2)
      /    \     /    \     /    \     /    \
pow(x,1) ... (总共8个pow(x,1)) 
  • 计算 pow(x, 8) 需要计算 2pow(x, 4)
  • 计算 pow(x, 4) 需要计算 2pow(x, 2)。因为有两个 pow(x, 4),所以总共需要计算 4pow(x, 2)
  • 计算 pow(x, 2) 需要计算 2pow(x, 1)。因为有四个 pow(x, 2),所以总共需要计算 8pow(x, 1)

您会发现,递归树的总节点数(即函数调用次数)与 n 成正比。对于 n = 8,调用了 15 次函数。对于 n,函数调用次数大约是 2 * n - 1

因此,时间复杂度是 O(n)


如何修改为 O(log n)?

正确的快速幂实现应该只计算一次 quick_power(x, n/2),然后将其结果复用。

修改后的代码:

double quick_power(double x, unsigned n) {
    if (n == 0) return 1.0;
    if (n == 1) return x;
    
    double half = quick_power(x, n / 2); // 关键:只递归调用一次,将结果存入变量
    double result = half * half; // 复用结果
    
    if (n & 1) { // 如果n是奇数,多乘一个x
        result *= x;
    }
    return result;
}

分析修改后的版本:

每次递归调用都将问题规模 n 减半(n -> n/2),并且每次只进行一次递归调用。

递归的深度是 log₂(n),每次递归只进行常数次操作(乘法、判断)。

因此,时间复杂度是 O(log n)

总结

版本时间复杂度原因
题目的版本O(n)对相同的子问题进行了重复的递归调用,递归树节点数与n成正比。
修改后的版本O(log n)每个子问题只计算一次,递归深度为 log(n)。

结论:您提供的 quick_power 函数的时间复杂度是 O(n)。 请使用修改后的版本来实现对数级的复杂度。

csp2023 阅读程序第一题

#include<iostream>
using namespace std;

unsigned short f(unsigned short x) {
    x ^= x << 6;  // 左移6位后与自身异或
    x ^= x >> 8;  // 右移8位后与自身异或
    return x;
}

int main() {
    unsigned short x;
    cin >> x;
    unsigned short y = f(x);
    cout << y << endl;
    return 0;
}

好的,我们来分析图片中的代码。这段代码实现了一个简单的线性反馈移位寄存器(LFSR)或类似的位操作函数,常用于生成伪随机数或进行简单的混淆/加密。

代码功能分析

#include<iostream>
using namespace std;

unsigned short f(unsigned short x) {
    x ^= x << 6;  // 左移6位后与自身异或
    x ^= x >> 8;  // 右移8位后与自身异或
    return x;
}

int main() {
    unsigned short x;
    cin >> x;
    unsigned short y = f(x);
    cout << y << endl;
    return 0;
}
函数 f 的操作分步解析:
  1. x ^= x << 6;

    • x 左移 6 位,然后与原始的 x 进行按位异或(XOR)操作。
    • 效果:这一步将 x 的高位信息扩散到低位。左移 6 位后,原始 x 的比特与它左边第 6 位的比特进行了混合。这是一种线性变换,增加了位的混淆。
  2. x ^= x >> 8;

    • 将上一步的结果右移 8 位,然后与自身进行异或。
    • 效果unsigned short 通常是 16 位。右移 8 位相当于将高 8 位移动到了低 8 位的位置。这一步将高字节的信息混合到低字节中。它与第一步结合,使得整个 16 位的所有比特都相互影响。
整体效果:

这个函数 f 是一个可逆的线性变换(因为使用的都是异或和移位,构成一个线性系统)。对于任意输入 x(16位),它输出一个 16 位的值 y。这个变换是单射和满射的,即每一个输入都对应一个唯一的输出,并且每一个输出也都对应一个唯一的输入。

时间复杂度分析

时间复杂度:O(1)

原因:

  1. 函数 f 内部的所有操作都是固定次数的位操作(两次移位、两次异或赋值)。
  2. 这些操作的执行时间与输入值 x 的大小无关。无论 x 是 0 还是 65535,计算机执行这两行代码的时钟周期数是相同的。
  3. 因此,该函数的时间复杂度是常数时间,记作 O(1)

空间复杂度分析

空间复杂度:O(1)

原因:

  1. 函数 f 只使用了固定数量的内存:一个输入参数 x(2字节)和一个返回值(2字节)。在计算过程中没有分配任何与输入规模相关的额外空间(如数组、递归调用栈等)。
  2. main 函数中也只使用了固定大小的变量 xy(各2字节)。
  3. 因此,该程序的辅助空间需求是常数,与输入值无关,空间复杂度为 O(1)

总结

方面分析结果原因
功能对输入的16位数进行一个可逆的线性混淆变换通过移位和异或操作,将数据的比特位充分混合。
时间复杂度O(1) (常数时间)只有固定次数的操作,执行时间不随输入变化。
空间复杂度O(1) (常数空间)只使用了固定数量的内存(几个变量),不随输入变化。

补充说明: 这种类型的代码常见于:

  • **伪随机数生成器(PRNG)**的组成部分。
  • 简单的哈希函数混淆函数
  • 嵌入式系统或硬件描述语言中的位操作技巧

当输入为“64”时,执行完第 5 行后X 的值是多少

好的,我们来逐步计算当输入 x = 64 时,执行完第5行(x ^= x << 6;)后 x 的值。

第一步:了解数据表示

  • unsigned short 通常是 16 位 无符号整数。
  • 输入值 64 的二进制表示:
    • 64 的二进制是 0000 0000 0100 0000(16位形式)。

第二步:执行第5行 x ^= x << 6;

这行代码是复合赋值,等价于 x = x ^ (x << 6)

  1. 计算 x << 6 (左移6位):

    • 原始 x: 0000 0000 0100 0000 (十进制 64)
    • 左移6位: 0001 0000 0000 0000 (十进制 4096)
      • 解释:0100 0000 (64) 向左移动6位,变成了 01 后面跟上10个0,即 0100 0000 0000 0000?不对,我们仔细计算:
      • 更准确的计算:64的二进制是 1000000(7位)。在16位中,是 0000000001000000
      • 左移6位:将 1000000 向左移6位,变成 1000000000000(13位),在16位中表示就是 0001000000000000(即 1<<12 = 4096)。
  2. 计算 x ^ (x << 6) (异或操作):

    • x: 0000 0000 0100 0000 (64)
    • x << 6: 0001 0000 0000 0000 (4096)
    • 异或结果: 0001 0000 0100 0000 (4160)
      • 异或规则:相同为0,不同为1。
      • 前8位:0000 0000 XOR 0001 0000 = 0001 0000
      • 后8位:0100 0000 XOR 0000 0000 = 0100 0000
      • 合并:0001 0000 0100 0000

第三步:验证结果

  • 二进制 0001 0000 0100 0000 转换为十进制:
    • 2^12 + 2^6 = 4096 + 64 = 4160。
  • 因此,执行完第5行后,x 的值是 4160

最终答案

当输入为“64”时,执行完第5行后,x 的值是 4160。

输入为512时,输出是多少

好的,我们来计算当输入为 512 时,程序的输出 y 是多少。

程序的核心是函数 f

unsigned short f(unsigned short x) {
    x ^= x << 6; // 第5行
    x ^= x >> 8; // 第6行
    return x;
}

输入 x = 512。我们需要逐步计算函数 f 的执行过程。

第一步:了解数据表示

  • unsigned short16 位 无符号整数。
  • 输入值 512 的 16 位二进制表示为:0000 0010 0000 0000
    • 因为 2⁹ = 512,所以第9位(从0开始计数)为1。

第二步:执行第5行 x ^= x << 6; (等价于 x = x ^ (x << 6))

  1. 计算 x << 6 (将 x 左移6位):

    • x: 0000 0010 0000 0000 (512)
    • x << 6: 1000 0000 0000 0000 (32768)
      • 将第9位的1向左移动6位,变成了第15位(最高位)为1。
  2. 计算 x ^ (x << 6) (异或操作):

    • x: 0000 0010 0000 0000 (512)
    • x << 6: 1000 0000 0000 0000 (32768)
    • 异或结果: 1000 0010 0000 0000 (33280)
      • 异或规则:相同为0,不同为1。
      • 最高位:0 XOR 1 = 1
      • 第9位:1 XOR 0 = 1
      • 其他位都是0

执行完第5行后,x 的值变为 33280

第三步:执行第6行 x ^= x >> 8; (等价于 x = x ^ (x >> 8))

  • 现在的 x33280,二进制为 1000 0010 0000 0000
  1. 计算 x >> 8 (将 x 右移8位):

    • x: 1000 0010 0000 0000 (33280)
    • x >> 8: 0000 0000 1000 0010 (130)
      • 将高8位 1000 0010 移动到低8位的位置。
  2. 计算 x ^ (x >> 8) (异或操作):

    • x: 1000 0010 0000 0000 (33280)
    • x >> 8: 0000 0000 1000 0010 (130)
    • 异或结果: 1000 0010 1000 0010 (33474)
      • 按位异或:高8位保持不变,低8位是 0000 0000 XOR 1000 0010 = 1000 0010

执行完第6行后,x 的值变为 33474

最终输出: 函数 f 返回 x 的值,即 33474

验证计算

我们可以用二进制计算来验证:

初始值: 0000 0010 0000 0000 (512)

第5行后:

  • 左移6位: 1000 0000 0000 0000 (32768)
  • 异或结果: 1000 0000 0000 0000 XOR 0000 0010 0000 0000 = 1000 0010 0000 0000 (33280)

第6行后:

  • 右移8位: 0000 0000 1000 0010 (130)
  • 异或结果: 1000 0010 0000 0000 XOR 0000 0000 1000 0010 = 1000 0010 1000 0010 (33474)

最终答案

当输入为"512"时,输出是 33474。

csp2023程序阅读第二题

#include <iostream>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;

// 使用筛法预处理并计算1~n中每个数的所有因子和之和 (∑σ(i))
long long solve1(int n) {
    // p数组:标记数组,初始为true。p[j]=true表示j尚未被处理
    vector<bool> p(n+1, true);
    // f数组:预处理阶段存储最小质因子,计算阶段重用于存储因子和σ(i)
    // g数组:存储最小质因子的最高幂次
    vector<long long> f(n+1, 0), g(n+1, 0);
    f[1] = 1;  // 1的因子和为1
    
    // 第一阶段:预处理最小质因子和最高幂次 (筛法)
    for (int i = 2; i * i <= n; i++) {
        if (p[i]) {  // 如果i是质数(尚未被标记)
            vector<int> d;
            // 生成i的所有幂次:i, i², i³, ... 直到不超过n
            for (int k = i; k <= n; k *= i) {
                d.push_back(k);
            }
            reverse(d.begin(), d.end());  // 反转,从大到小排序
            
            // 从大到小处理每个幂次
            for (int k : d) {
                // 标记所有k的倍数
                for (int j = k; j <= n; j += k) {
                    if (p[j]) {  // 如果j还没有被处理过
                        p[j] = false;    // 标记为已处理
                        f[j] = i;        // 记录最小质因子
                        g[j] = k;        // 记录最小质因子的最高幂次
                    }
                }
            }
        }
    }
    
    // 处理大于sqrt(n)的质数
    for (int i = sqrt(n) + 1; i <= n; i++) {
        if (p[i]) {      // 如果i仍然是质数
            f[i] = i;    // 最小质因子就是自身
            g[i] = i;    // 最高幂次就是自身(质数的1次方)
        }
    }
    
    // 第二阶段:利用积性计算每个数的因子和
    long long sum = 1;  // 已经包含σ(1)=1
    for (int i = 2; i <= n; i++) {
        // 关键公式:利用因子和的积性性质
        // 如果 i = p^k * m (p是最小质因子,gcd(p, m)=1)
        // 那么 σ(i) = σ(p^k) * σ(m) = (p^{k+1}-1)/(p-1) * σ(m)
        // 这里:g[i] = p^k, f[i] = p, i/g[i] = m
        f[i] = f[i / g[i]] * (g[i] * f[i] - 1) / (f[i] - 1);
        sum += f[i];  // 累加每个数的因子和
    }
    return sum;  // 返回所有因子和的总和
}

// 直接计算:∑(i=1 to n) i * ⌊n/i⌋
// 数学上等于 ∑(i=1 to n) σ(i) (所有数的因子和之和)
long long solve2(int n) {
    long long sum = 0;
    for (int i = 1; i <= n; i++) {
        // 对于每个数i,它在1~n中作为因子出现的次数是⌊n/i⌋
        // 因此贡献为 i * ⌊n/i⌋
        sum += i * (n / i);
    }
    return sum;  // 返回计算结果
}

int main() {
    int n;
    cin >> n;  // 输入n
    
    // 输出两个函数的结果(理论上应该相同)
    cout << solve1(n) << endl;  // 使用筛法计算
    cout << solve2(n) << endl;  // 使用直接方法计算
    
    return 0;
}

csp2022第二题

你同时用 time 命令和秒表为某个程序在单核 CPU 的运行计时。假如 time 命令的输出如下: real 0m30.721s user 0m24.579s sys 0m6.123s 以下最接近秒表计时的时长为( )?

根据 time 命令的输出:

  • real: 0m30.721s (实际流逝的时间,即秒表计时)
  • user: 0m24.579s (用户态CPU时间)
  • sys: 0m6.123s (内核态CPU时间)

real time(实际时间) 是指从程序开始到结束所经过的墙钟时间(wall-clock time),也就是秒表测量的时间。它包括了:

  • 程序在CPU上运行的时间(user + sys)
  • 程序等待的时间(例如I/O操作、阻塞、调度等待等)

因此,最接近秒表计时的时长就是 real time,即 30.721 秒

为什么?

  • usersys 时间之和(24.579 + 6.123 = 30.702s)非常接近 real 时间(30.721s),说明程序几乎一直在CPU上运行,等待时间很少(仅相差0.019s)。
  • 但秒表测量的是实际流逝的时间,所以就是 real 时间。

答案:30.721 秒

csp2022第7题

一个深度为 5(根结点深度为 1)的完全 3 叉树,按前序遍历的顺序给结点从 1 开始编号,则第 100 号结点的父结点是第( )号。

树一共5层,每层结点数分别为1,3,9,27,81。 1层满三叉树共1个结点 2层满三叉树共4个结点 3层满三叉树共13个结点 4层满三叉树共40个结点 按照前序遍历顺序先遍历结点1的第一个子树,该子树是4层满三叉树,共40个结点。遍历第二个子树,也是4层满三叉树,共40个结点,因此1的第三个孩子的编号为82。 结点82的第一个子树为3层满三叉树,共13个结点。 82的第二个孩子编号为96。96的第一个孩子编号为97。97是第5层的结点,97的三个孩子编号分别为98,99,100。 因此100的父结点编号是97。

三叉树.JPG

强联通图

强连通图的性质不包括( ):

A. 每个顶点的度数至少为 1

B. 任意两个顶点之间都有边相连

C. 任意两个顶点之间都有路径相连

D. 每个顶点至少都连有一条边

我们来分析强连通图的性质以及每个选项:

强连通图(针对有向图)的定义:一个有向图是强连通的,当且仅当图中任意两个顶点 (u) 和 (v) 之间都存在一条从 (u) 到 (v) 的路径和一条从 (v) 到 (u) 的路径。

现在检查每个选项:

  • A. 每个顶点的度数至少为1
    如果某个顶点的出度或入度为0,那么它可能无法与某些顶点相互到达(例如,如果出度为0,则无法到达其他顶点;如果入度为0,则其他顶点无法到达它)。因此,强连通图中每个顶点必须至少有一条出边和一条入边(即每个顶点的出度和入度都至少为1),所以度数至少为2(因为度数包括出度和入度)。但选项说“度数至少为1”,这不一定正确(因为可能只有出度或只有入度为1,但这样不满足强连通)。实际上,强连通图要求每个顶点的出度和入度都至少为1,因此总度数至少为2。所以A性质不包括(即不一定成立)。

  • B. 任意两个顶点之间都有边相连
    这是完全图的定义,不是强连通图的必要条件。强连通图只要求有路径相连,不一定直接有边。例如,三个顶点形成有向环(a→b, b→c, c→a)是强连通的,但任意两个之间不一定有直接边(a和c之间没有直接边)。所以B不包括。

  • C. 任意两个顶点之间都有路径相连
    这正是强连通图的定义(注意是有向路径,并且是双向的)。所以C正确。

  • D. 每个顶点至少都连有一条边
    强连通图中每个顶点必须至少有一条出边和一条入边(否则无法与其他顶点相互到达),因此每个顶点至少连有两条边(出边和入边)。但选项说“至少连有一条边”,这虽然成立(因为至少两条边当然包含至少一条边),但这不是强连通图的特有性质(非强连通图也可能有)。然而,题目是问“不包括”,即哪个不是强连通图的性质。实际上D是成立的(每个顶点至少有一条边),但可能不是最精确的。但相比A和B,D是成立的。

但仔细分析:强连通图要求每个顶点出度和入度都至少为1,因此每个顶点至少连有两条边(出边和入边)。所以“每个顶点至少都连有一条边”显然成立(因为至少两条)。所以D是性质。

而A说“度数至少为1”:实际上强连通图要求每个顶点度数至少为2(出度和入度各至少1),所以度数至少为1虽然成立,但不够准确?但题目是“不包括”,即哪个不是性质。实际上A也是成立的(因为至少2肯定包含至少1)。但可能出题者意图是“度数至少为1”不是强连通图特有的(因为非强连通图也可能有),但严格来说A是成立的。

但对比B:B明显不是性质(强连通不一定有直接边)。

实际上,最不包括的是B,因为强连通不要求直接边。

再看A:强连通图每个顶点度数至少为2(有向图中度数=出度+入度),所以“至少为1”成立,但不如至少2精确。但选项A说“至少为1”是正确的(因为2>=1)。所以A也是性质。

但题目是“不包括”,所以B肯定不包括。

另外,C和D都是包括的性质。

所以答案应该是B。

为什么A是性质?
因为强连通图每个顶点度数至少为2(出度和入度都>=1),所以当然度数至少为1。所以A是性质。

为什么D是性质?
每个顶点至少有一条出边和一条入边,所以至少连有两条边,当然至少一条边。所以D是性质。

因此,不包括的是B。

解释: 强连通图要求任意两个顶点之间有路径相连(但不一定直接有边),因此B“任意两个顶点之间都有边相连”不是强连通图的性质(这是完全图的性质)。

欧拉图

每个顶点度数均为 2 的无向图称为“2 正规图”。由编号为从1到n的顶点构成的所有2 正规图,其中包含欧拉回路的不同 2 正规图的数量为( )。

我们需要计算由编号为1到n的顶点构成的所有2-正则图中,包含欧拉回路的不同2-正则图的数量。

关键点:

  1. 2-正则图:每个顶点的度数均为2。这样的图由若干个不相交的环(圈)组成。

  2. 欧拉回路:存在一条经过每条边恰好一次并回到起点的回路。对于无向图,欧拉回路存在的充要条件是图连通且所有顶点度数为偶数(这里度数都是2,满足偶数)。但注意:欧拉回路要求图是连通的(即整个图是一个环),否则如果图有多个连通分量(多个环),则不存在经过所有边的欧拉回路(但每个环自身有欧拉回路,但整个图没有)。

    • 因此,包含欧拉回路的2-正则图必须是一个单一的环(即整个图是一个长度为n的环)。
  3. 问题转化为:计算顶点编号为1到n的不同的单一环(即哈密顿环)的数量。

    • 注意:环是同构的(旋转和翻转相同),但这里顶点有编号(标记顶点),所以不同编号的环视为不同。
  4. 计算不同的环的数量

    • 固定顶点1,然后排列剩下的n-1个顶点:有(n-1)!种排列。
    • 但每个环有两种方向(顺时针和逆时针),且旋转后相同?不对,因为顶点编号不同,所以每个环被计算了2次(因为方向)?实际上:
      • 对于标记顶点的环,不同的环的数量是(n-1)! / 2。
    • 但这里:顶点编号为1到n,考虑所有以1为起点,然后排列其他顶点,但环可以翻转(对称),所以每个环被计算了2次(方向)。
    • 因此,不同的环的数量为:(n-1)!/2

然而,我们重新思考:

  • 实际上,对于标记顶点的环,固定一个顶点(比如1),然后排列其他n-1个顶点,但这样每个环被计算了2次(因为顺时针和逆时针是相同的环?但标记顶点时,方向不同的环通常视为不同?不,在无向图中,环没有方向,所以顺时针和逆时针是同一个环。
  • 所以:固定顶点1,然后排列其他n-1个顶点,但这样每个环被计算了2次(因为环可以从两个方向生成)。
  • 因此,不同的环的数量是:(n-1)!/2.

但是:问题问的是“不同的2-正规图”,而2-正则图可能由多个环组成。但这里要求包含欧拉回路,所以必须是一个环(连通)。

所以答案应该是:(n-1)!/2.

验证小n:

  • n=3:只有一个三角形环,但顶点编号:有1种环?实际上:环123和132是相同的(无向),所以只有1个。公式:(3-1)!/2=2/2=1。正确。
  • n=4:不同的环:固定1,排列2,3,4:有3!=6种排列,但每个环被计算2次(方向),所以6/2=3个环。例如:1234,1243,1324(但1243和1342相同?)。实际上,确实有3个不同的环(标记顶点)。

因此,包含欧拉回路的不同2-正则图的数量为(n-1)!/2.

排列组合

共有 8 人选修了程序设计课程,期末大作业要求由 2 人组成的团队完成。假设不区分每个团队内 2 人的角色和作用,请问共有多少种可能的组队方案( )。

不区分角色和作用,即为组合,所以直接C(8,2)= 87/21=28

时间复杂度分析

对于给定的 n,分析以下代码段对应的时间复杂度,其中最为准确的时间复杂度为( )。

  int i,j,k = 0;
	for(int i = 0; i < n; i++){
		for(int j = 1; j < n; j*=2;){
			k = k + n / 2;
		}
	}

i从0~n-1循环,循环n次 j从1到n-1,j每次乘以2,j每次循环乘以2直至达到n,循环的次数为 每层循环的循环次数相乘,是总的循环次数。

排序算法的时间复杂度

image.png

找最大值最大值

以比较为基本运算,对于2n个数,同时找到最大值和最小值,最坏情况下需要的最少的比较次数为多少?

对于2n个数,同时找到最大值和最小值,最坏情况下需要的最少的比较次数为3n-2。以下是详细解释:

  • 将2n个数分成n对,每对进行一次比较,共n次比较,得到每对的较大者和较小者。
  • 从n个较大者中找到最大值,需要n-1次比较。
  • 从n个较小者中找到最小值,需要n-1次比较。
  • 总比较次数为:n + (n-1) + (n-1) = 3n-2。

该算法是最优的,因为理论上已证明,对于n个数(n为偶数),同时找到最大值和最小值的最坏情况下最少比较次数为3n/2 - 2,但这里n表示对数,总数为2n,因此结果为3n-2。

无向图

G是一个非连通简单无向图(没有自环和重边),共有36条边,则该图至少有()个点

对于一个非连通简单无向图,共有36条边,要找到最小的顶点数,图必须是非连通的,因此至少有两个连通分量。为了最小化顶点数,应使一个连通分量尽可能大(以容纳更多边),另一个连通分量尽可能小(但至少有一个顶点)。

完全图 (K_k) 的边数为 (\frac{k(k-1)}{2})。需要 (\frac{k(k-1)}{2} \geq 36),即 (k(k-1) \geq 72)。解得 (k \geq 9),因为 (9 \times 8 / 2 = 36)。因此,一个连通分量可以是 (K_9),具有36条边。

另一个连通分量至少有一个顶点(孤立点),贡献0条边。因此,总顶点数为 (9 + 1 = 10)。

如果顶点数小于10(例如9),则图的最大边数为 (\frac{9 \times 8}{2} = 36),但这是连通图(完全图 (K_9))。任何非连通图在9个顶点时边数都小于36,无法满足条件。

因此,该图至少有10个点。

组合4位数

由数字1、1、2、4、8、8组成的不同的4位数的个数是多少?

由数字1、1、2、4、8、8组成不同的四位数,需要从这些数字中选取4个数字进行排列。由于数字有重复,需考虑所有可能的多重集组合及其排列数。

可能的多重集组合及其排列数如下:

  • {1,2,4,8}:所有数字不同,排列数为 (4! = 24)。
  • {1,1,2,4}:有两个1相同,排列数为 4!/2! = 12。
  • {1,1,2,8}:有两个1相同,排列数为 4!/2! = 12。
  • {1,1,4,8}:有两个1相同,排列数为 4!/2! = 12。
  • {1,2,8,8}:有两个8相同,排列数为 4!/2! = 12。
  • {1,4,8,8}:有两个8相同,排列数为 4!/2! = 12。
  • {2,4,8,8}:有两个8相同,排列数为 4!/2! = 12。
  • {1,1,8,8}:有两个1和两个8相同,排列数为 4!/2!*2! = 6.

总排列数为: [ 24 + 12 + 12 + 12 + 12 + 12 + 12 + 6 = 102 ]

因此,不同的四位数的个数为102。

等比数列

有一个等比数列,共有奇数项,其中第一项和最后一项分别是2和118098,中间一项是486,请问一下哪个数是可能的公比?

等比数列.JPG

二分图

二分图是指能将顶点划分为两个部分,每一部分内的顶点间没有边相连的简单无向图。那么24个顶点的二分图至多有多少条边

对于24个顶点的二分图,边数的最大值取决于将顶点划分为两个部分时,两部分大小的乘积。设两部分的大小分别为 a和b,且a + b = 24。边数最大当 a 和 b 尽可能相等,即a = 12 和 b = 12,此时边数为 ab=1212=144。

因此,24个顶点的二分图至多有144条边。