判断子序列 & 二进制位操作 & 二进制中1的个数 & unique函数的实现

判断子序列

问题描述

给定两个整数序列 ab,长度分别为 nm。判断 a 是否为 b 的子序列。子序列的定义是原序列中一部分项按原有次序排列的序列,例如 {a1, a3, a5}{a1, a2, a3, a4, a5} 的一个子序列。

解题思路

使用双指针法遍历两个序列。指针 i 指向序列 a,指针 j 指向序列 b。通过比较 a[i]b[j] 的值,逐步移动指针,最终判断 a 是否完全匹配 b 的某个子序列。

代码实现

#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int n, m;
int a[N], b[N];

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);
    for (int i = 0; i < m; i++) scanf("%d", &b[i]);

    int i = 0, j = 0;
    while (i < n && j < m) 
    {
    	//n <= m,i指向子串,j指向原串
        //匹配时i,j都向后移动;
        //不匹配时,j向后移动
        if (a[i] == b[j]) i++;
        j++;
    }

    if (i == n) cout << "Yes";
    else cout << "No";

    return 0;
}

代码解析

  1. 输入处理

    • 读取序列 a 的长度 n 和序列 b 的长度 m
    • 分别读取序列 ab 的所有元素。
  2. 双指针遍历

    • 初始化指针 ij 为 0,分别指向序列 ab 的起始位置。
    • 在循环中比较 a[i]b[j]
      • 如果相等,移动指针 i(表示匹配到一个元素)。
      • 无论是否匹配,移动指针 j(继续检查 b 的下一个元素)。
  3. 结果判断

    • 如果 i 等于 n,说明 a 的所有元素都在 b 中按顺序匹配成功,输出 “Yes”。
    • 否则,输出 “No”。

复杂度分析

  • 时间复杂度:O(m),因为需要遍历整个序列 b
  • 空间复杂度:O(1),仅使用了常数个额外变量。

示例测试

示例 1

输入:

3 5
1 3 5
1 2 3 4 5

输出:

Yes

解释:序列 {1, 3, 5}{1, 2, 3, 4, 5} 的子序列。

示例 2

输入:

4 6
1 4 5 7
1 2 3 4 5 6

输出:

No

解释:序列 {1, 4, 5, 7} 不是 {1, 2, 3, 4, 5, 6} 的子序列(7 不存在于 b 中)。


二进制位操作的原理与实现

在计算机科学中,二进制位操作是一种基础且重要的技术。理解如何提取二进制数的某一位对于处理底层数据结构和算法至关重要。以下内容将详细解释二进制位操作的原理,并提供相应的代码实现。

二进制位的基本概念

一个二进制数由多个位组成,每个位只能是0或1。例如,二进制数1101可以分解为:

  • 第3位:1
  • 第2位:1
  • 第1位:0
  • 第0位:1

提取二进制某一位的方法

要提取二进制数的第k位,可以通过以下步骤实现:

  1. 将二进制数右移k位,使得目标位移动到最低位(第0位)。
  2. 对移动后的数与1进行按位与(AND)运算。这将屏蔽其他位,仅保留最低位的值。

数学表达式为:
第k位的值 = (n >> k) & 1

代码实现

以下是一个完整的C++示例,展示如何提取一个数的每一位并打印其二进制表示:

#include <iostream>
using namespace std;

const int N = 100010;
int a[N];

int main() {
    int n = 10; // 要转换的数字
    // 输出每一位,从最高位开始
    for (int k = 3; k >= 0; k--) {
        printf("%d", (n >> k) & 1);
    }
    return 0;
}

代码解析

  1. 变量定义n为待处理的十进制数,这里以10为例。
  2. 循环结构:循环从最高位(第3位)开始,逐步处理到最低位(第0位)。
  3. 位操作:每次循环中,将n右移k位后与1进行按位与运算,结果即为第k位的值。
  4. 输出:直接打印每一位的值,最终得到n的二进制表示。

应用场景

这种方法在以下场景中非常有用:

  • 二进制数的解析与转换。
  • 位掩码的设计与实现。
  • 硬件编程中的寄存器操作。

总结

通过右移和按位与运算,可以高效地提取二进制数的任意一位。这种方法不仅简单,而且计算速度快,适合在各种编程环境中使用。


二进制中1的个数

问题描述

给定一个长度为n的数列,要求计算每个数的二进制表示中1的个数。

解决方法

方法一:逐位检查法

通过移位操作逐位检查每一位是否为1。对于32位整数,最多需要检查32次。

int countBits(int num) {
    int count = 0;
    for (int i = 0; i < 32; ++i) {
        if (num & (1 << i)) {
            count++;
        }
    }
    return count;
}

方法二:lowbit优化法

利用二进制补码特性快速定位最右侧的1,每次减去该位直到数值归零,统计操作次数即为1的个数。

int lowbit(int x) {
    return x & (-x); // 等价于x & (~x + 1)
}

int countBits(int x) {
    int count = 0;
    while (x) {
        x -= lowbit(x);
        count++;
    }
    return count;
}

方法三:Brian Kernighan算法

通过n & (n-1)直接消除最右侧的1,循环次数等于1的个数,效率更高。

int countBits(int n) {
    int count = 0;
    while (n) {
        n &= (n - 1);
        count++;
    }
    return count;
}

方法四:查表法(空间换时间)

预处理0-255所有数的1的个数,分段查表减少计算次数,适合大规模数据。

int table[256];
void initTable() {
    for (int i = 0; i < 256; ++i) {
        table[i] = (i & 1) + table[i >> 1];
    }
}

int countBits(int n) {
    return table[n & 0xFF] + table[(n >> 8) & 0xFF] + 
           table[(n >> 16) & 0xFF] + table[(n >> 24) & 0xFF];
}

性能对比

方法时间复杂度适用场景
逐位检查法O(32)简单通用
lowbit优化法O(k), k为1的个数需要快速定位1
Brian Kernighan法O(k)高效消除1
查表法O(1)大规模数据预处理

完整代码示例

结合lowbit方法实现输入输出:

#include<iostream>
using namespace std;

const int N = 100010;
int a[N];

int lowbit(int x) {
    return x & -x;
}

int main() {
    int n;
    scanf("%d", &n);
    while (n--) 
    {
        int x, count = 0;
        scanf("%d", &x);
        while (x) 
        {
            x -= lowbit(x);
            count++;
        }
        printf("%d ", count);
    }
    return 0;
}

扩展思考

  • 负数处理:上述方法均适用于补码表示的负数。
  • 并行计算:可通过位操作并行统计多个位的1的个数。
  • 硬件指令:部分CPU提供POPCNT指令可直接获取1的个数。

unique函数的实现

unique函数的分析

实现一个高效的去重函数是算法和数据处理中的常见需求。以下是一个针对已排序容器的unique函数实现,其核心思想是将不重复元素移动到容器前部,并返回无重复元素部分的结束迭代器。

实现代码

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

vector<int>::iterator unique(vector<int> &a)
 {
 	//j <= i
    //j指向的是不重复元素的下一个位置
    //i是用来遍历整个数组的
    int j = 0;
    for (int i = 0; i < a.size(); i++)
     {
     	//如果是第一个元素或你和前面的元素不等,那么你就是不重复元素
        if (i == 0 || a[i] != a[i - 1]) 
        {
            a[j] = a[i];
            j++;
        }
        //[0, j - 1]是不重复元素
    }
    //返回的是最后一个不重复元素的下一个位置
    return a.begin() + j;
}

int main() 
{
    vector<int> a = {1, 1, 1, 2, 3, 4, 4, 4, 4, 5, 5, 6};
    auto iter = unique(a);
    for (auto it = a.begin(); it < iter; it++)
    {
        cout << *it << " ";
    }
    cout << endl;
    return 0;
}

实现原理

  • 输入要求:容器必须是已排序的,否则无法保证正确去重。
  • 双指针法:使用变量j指向不重复元素的下一个位置,i用于遍历整个数组。
  • 条件判断:当前元素是第一个元素或与前一个元素不相同时,将其移动到位置j并递增j
  • 结果范围[0, j - 1]为不重复元素的范围,函数返回a.begin() + j作为结束迭代器。
示例输出

输入容器为{1, 1, 1, 2, 3, 4, 4, 4, 4, 5, 5, 6}时,输出为:

1 2 3 4 5 6 

复杂度分析

  • 时间复杂度:O(n),仅需一次遍历。
  • 空间复杂度:O(1),原地操作,无需额外空间。

扩展说明

  • 与STL的std::unique对比:STL的实现原理类似,但返回的是逻辑上无重复的结束迭代器,原容器剩余部分的值未定义。
  • 非排序容器的去重:需先排序(O(nlogn)),再使用此方法或哈希表法(O(n)空间)。

注意事项

  • 确保容器已排序,否则结果错误。
  • 函数不改变容器大小,仅逻辑上标记无重复部分,可通过erase清理剩余部分。

算法内容来自AcWing算法基础课,感谢AcWing老师的详细讲解。

我们来逐步分析并解决这个问题。 --- ### **问题解析** 我们要找的是:**在小于等于 n 的正整数中,最大的一个被称为&ldquo;镜&rdquo;的数字。** 什么是&ldquo;镜&rdquo;? - 在二进制表示下,0 和 1个数相等。 - 注意:二进制表示是标准的,不带前导零(除了数字 0 本身)。 - 所以,&ldquo;镜&rdquo;数必须满足: - 二进制1个数 = 0 的个数。 注意:由于二进制没有前导零,比如 `6` 是 `110`,它有 2 个 11 个 0,不是&ldquo;镜&rdquo;。 但 `9` 是 `1001` &rarr; 2 个 1,2 个 0 &rarr; 是&ldquo;镜&rdquo;。 又如 `10` 是 `1010` &rarr; 也是&ldquo;镜&rdquo;。 所以&ldquo;镜&rdquo;数的必要条件是:其二进制长度为偶数(因为 1 和 0 数量相等),且 1 的数量 = 0 的数量 = len/2。 --- ### **目标** 对于每个查询 `n`,找出 &le; n 的最大&ldquo;镜&rdquo;数。 --- ### **解法思路** #### 方法一:预处理所有&ldquo;镜&rdquo;数 由于题目没给 `n` 的范围,但从算法角度考虑: - 最大可能的 `n` 假设是 $ 10^9 $ 或 $ 2^{30} \approx 1e9 $ - 那么二进制最多 30 位 - 我们可以枚举所有长度为偶数的二进制位数(2, 4, 6, ..., 30) - 对于每个长度 `L`,生成所有长度为 `L`、恰好包含 `L/2` 个 1二进制数(不能有前导零 &rarr; 第一位必须是 1) - 将这些数转换成十进制,并加入集合 - 然后对所有生成的&ldquo;镜&rdquo;数排序 - 每次查询时,在有序数组中用二分查找找到 &le; n 的最大值 这样时间复杂度可控,因为组合数不会爆炸(最多 C(29,14) &asymp; 40 多万,实际更少) 但我们可以优化:只生成合法的二进制串(第一位为 1,其余位置选 L/2 - 11) --- ### **C++ 实现** ```cpp #include &lt;iostream&gt; #include &lt;vector&gt; #include &lt;algorithm&gt; #include &lt;string&gt; using namespace std; vector&lt;long long&gt; mirrorNumbers; // 生成所有长度为 len 的二进制数,其中 1个数为 cnt1,且第一位是 1 void generate(int len, int ones, string current) { if (current.size() == len) { if (ones == 0) { long long num = stoll(current, nullptr, 2); mirrorNumbers.push_back(num); } return; } int zeros = len - current.size() - ones; // 剪枝:如果剩下的位置不够放 1 或 0,返回 if (ones &lt; 0 || zeros &lt; 0) return; // 第一位必须是 &#39;1&#39; if (current.empty()) { generate(len, ones - 1, &quot;1&quot;); return; } // 尝试填 &#39;1&#39; if (ones &gt; 0) { generate(len, ones - 1, current + &#39;1&#39;); } // 尝试填 &#39;0&#39; if (zeros &gt; 0) { generate(len, ones, current + &#39;0&#39;); } } void precompute() { // 枚举所有偶数长度:2, 4, 6, ..., 30 for (int len = 2; len &lt;= 30; len += 2) { generate(len, len / 2, &quot;&quot;); } sort(mirrorNumbers.begin(), mirrorNumbers.end()); // 去重(理论上不需要,但保险) mirrorNumbers.erase(unique(mirrorNumbers.begin(), mirrorNumbers.end()), mirrorNumbers.end()); } int main() { precompute(); int Q; cin &gt;&gt; Q; while (Q--) { long long n; cin &gt;&gt; n; // 二分查找最后一个 &lt;= n 的数 auto it = upper_bound(mirrorNumbers.begin(), mirrorNumbers.end(), n); if (it == mirrorNumbers.begin()) { // 没有符合条件的数 cout &lt;&lt; 0 &lt;&lt; &#39;\n&#39;; } else { --it; cout &lt;&lt; *it &lt;&lt; &#39;\n&#39;; } } return 0; } ``` --- ### **解释** 1. **`precompute()` 函数**: - 预先生成所有可能的&ldquo;镜&rdquo;数(二进制长度为 2 到 30 的偶数) - 调用 `generate` 来递归构造二进制字符串 - 第一位强制为 `&#39;1&#39;`(避免前导零) - 只有当剩余 1 和 0 的数量非负时才继续递归 2. **`generate()` 函数**: - 使用回溯法生成所有满足条件的二进制串 - 当长度达到 `len` 且 1 的数量刚好用完时,将其转为十进制存入全局列表 3. **查询处理**: - 对每个 `n`,使用 `upper_bound` 找到第一个大于 `n` 的数 - 前一个就是答案 - 如果没有 &le; n 的&ldquo;镜&rdquo;数,则输出 0(虽然题目说正整数,但若无解可能需特殊处理) --- ### **边界情况示例** - `n = 1` &rarr; 二进制 `1` &rarr; 11,0 个 0 &rarr; 不平衡 &rarr; 无解? 最小&ldquo;镜&rdquo;是 `10`₂ = `2`,但 `10` 是 `2`,但 `10` 只有 111 个 0 &rarr; 是&ldquo;镜&rdquo;! - `2` 的二进制是 `10` &rarr; 111 个 0 &rarr; 是&ldquo;镜&rdquo; - 所以 `n=2` 应输出 `2` - `n=1` 时,没有满足条件的正整数(&le;1),所以输出可能是 0 或无解。根据题意&ldquo;正整数&rdquo;,如果没有应输出什么? - 但样例未说明,保守输出 **0** --- ### **性能分析** - 最大长度 30,一半是 15 - 总共生成的数大约是: - len=2: C(1,0)=1 - len=4: C(3,1)=3 - len=6: C(5,2)=10 - ... - len=30: C(29,14) &asymp; 37 million? 实际不可能全生成! ⚠️ **注意:上面的递归会超时或爆栈吗?** 其实 `C(29,14)` 约为 4000 万左右,太大了,不能暴力生成! 我们需要优化! --- ### **改进方法:使用 `std::next_permutation`** 我们可以利用 STL 生成所有含 `k` 个 1 和 `k` 个 0 的二进制串(首位为 1) ```cpp void generateWithPermutation(int k) { string s = string(k, &#39;1&#39;) + string(k-1, &#39;0&#39;); // 第一位固定为 1,后面 k-11 和 k 个 0? // 错了:总共要 k 个 1 和 k 个 0,第一位是 1 &rarr; 剩下 k-11 和 k 个 0 string tail = string(k-1, &#39;1&#39;) + string(k, &#39;0&#39;); sort(tail.begin(), tail.end()); do { string full = &quot;1&quot; + tail; long long num = 0; for (char c : full) { num = num * 2 + (c - &#39;0&#39;); } if (num &gt; 0) mirrorNumbers.push_back(num); } while (next_permutation(tail.begin(), tail.end())); } ``` 但 `next_permutation` 会对重复字符产生重复排列?不会,它能正确处理重复。 但是 `k` 最大 15 &rarr; `tail` 长度 2k-1 = 29,`k=15` &rarr; `tail` 有 14 个 115 个 0 &rarr; 共 29 个字符 &rarr; 组合数 C(29,14) &asymp; 4000 万,太多! 所以我们只能生成较小的 `k`,或者限制最大 `n`。 但题目中 `n` 最大可能为 1e9,对应约 2^30,但我们只需生成 &le; 1e9 的&ldquo;镜&rdquo;数即可。 所以可以在生成时判断数值是否超过阈值(如 2e9)就跳过。 --- ### **最终优化版本:限制范围生成** ```cpp #include &lt;iostream&gt; #include &lt;vector&gt; #include &lt;algorithm&gt; #include &lt;string&gt; #include &lt;cctype&gt; using namespace std; vector&lt;long long&gt; mirrorNumbers; void generateValid(int k) { if (k == 0) return; // 构造尾部:k-1&#39;1&#39; 和 k 个 &#39;0&#39; string tail = string(k-1, &#39;1&#39;) + string(k, &#39;0&#39;); sort(tail.begin(), tail.end()); do { string binStr = &quot;1&quot; + tail; long long num = 0; for (char c : binStr) { if (num &gt; (long long)2e9) break; // 提前退出 num = num * 2 + (c - &#39;0&#39;); } if (num &lt;= (long long)2e9) { mirrorNumbers.push_back(num); } } while (next_permutation(tail.begin(), tail.end())); } void precompute() { // k 从 115 左右 for (int k = 1; k &lt;= 15; ++k) { generateValid(k); } sort(mirrorNumbers.begin(), mirrorNumbers.end()); auto last = unique(mirrorNumbers.begin(), mirrorNumbers.end()); mirrorNumbers.erase(last, mirrorNumbers.end()); } int main() { precompute(); int Q; cin &gt;&gt; Q; while (Q--) { long long n; cin &gt;&gt; n; auto it = upper_bound(mirrorNumbers.begin(), mirrorNumbers.end(), n); if (it == mirrorNumbers.begin()) { cout &lt;&lt; 0 &lt;&lt; &#39;\n&#39;; } else { cout &lt;&lt; *(--it) &lt;&lt; &#39;\n&#39;; } } return 0; } ``` --- ### **总结** - 使用 `next_permutation` 生成所有以 1 开头、包含 k 个 1 和 k 个 0 的二进制数 - 转换为十进制并存储 - 排序去重 - 查询时二分查找 这个方法效率较高,适用于 `n &le; 1e9` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值