【算法导论】XHS 0824 笔试题解

行为权重

小红是小红书的用户行为分析师。平台将每次用户行为映射为一个正整数权重序列 {a1,a2,...,an},以便后续关联推荐时提取关键"红色"行为。

为了保证标记的行为具有足够的共性,必须选出的所有"红色"行为权重的最大公约数大于 1;同时,为了避免相邻行为产生冗余,所选下标不得相邻。

现给定用户的一次行为序列,求最多可以染成红色的行为数量。

最大公约数:指一组整数共有约数中最大的一个。例如,12、18 和 30 的公约数有 1,2,3,6,其中最大的约数是 6,因此gcd(12,18,30)=6。

输入输出

输入描述:

在一行上输入一个整数 n(1≤n≤10^5),表示行为序列长度。

在第二行输入 n 个整数 a[1],a[2],...,a[n] (1≤a[i]≤100),表示每次行为的权重值。

输出描述:

在一行上输出一个整数,表示最多可染红的行为数量。

测试样例

输入

复制代码
5
1 2 3 2 6

输出

复制代码
2

在这个样例中,可将下标2与4对应的权重染红,它们的最大公约数为2,且不相邻,故答案为2。

输入

复制代码
4
2 4 9 6

输出

复制代码
2

超时解法

由于对输入数组中第i个元素的选择决策,可能会导致已被选择的元素的最大公约数发生变化,从而影响对数组中第i+1个元素直到第n个元素的决策,因此从表面上来看本题并不符合"最优子结构"的特点,并不适用DP,看上去只能无脑进行递归搜索。

很可惜,这种方法只能通过30%的测试用例。

c++ 复制代码
#include <bits/stdc++.h>

#define INF (std::numeric_limits<int>::max())

int ans = 0;

void Solve(const std::vector<int>& a, int pos, int gcd, bool last_is_printed, int count) {
    if (pos == (int)a.size()) {
        ans = std::max(ans, count);
        return;
    }
    
    if (count + (int)a.size() - pos + 1 < ans) {
        return;
    }
    
    // 染色
    if (gcd == INF) {
        // 如果gcd未初始化
        if (a[pos] > 1) {
            Solve(a, pos + 1, a[pos], true, count + 1);
        }
    } else {
        int new_gcd = std::gcd(gcd, a[pos]);
        if (!last_is_printed && new_gcd > 1) {
            Solve(a, pos + 1, new_gcd, true, count + 1);
        }
    }
    
    // 不染色
    Solve(a, pos + 1, gcd, false, count);
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);
    
    int n;
    std::cin >> n;
    
    std::vector<int> a(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> a[i];
    }
    
    Solve(a, 0, INF, false, 0);
    
    std::cout << ans;
}

正确解法

让我们重新审视一下题目中的关键约束条件:在原数组a的所选子序列x中,所有元素x[i]的最大公约数G_max>1。

这句话实际上等价于:在原数组a的所选子序列x中,所有元素x[i]至少存在一个>1的公约数G。

另外我们注意到本题中1≤a[i]≤100,这意味着一定有1<G≤x[i]≤100,这对于G来说是一个非常小的数据范围。

因此我们可以采用固定变量法,在外循环中枚举可能的G。在内循环中,由于G固定,问题就退化成了一个类似于"打家劫舍"的简单的一维DP问题。我们只要取全体这些子问题的答案的最大值,即为原问题的最终答案。

C++ 复制代码
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n;
    std::cin >> n;
    std::vector<int> a(n);
    int max_elem = 0;
    for (int i = 0; i < n; ++i) {
        std::cin >> a[i];
        max_elem = std::max(max_elem, a[i]);
    }

    int ans = 0;
    for (int g = 2; g <= max_elem; ++g) {
        std::vector<int> dp(n);
        dp[0] = (a[0] % g == 0) ? 1 : 0;
        for (int i = 1; i < n; ++i) {
            // 当g是a[i]的公约数时,a[i]可选可不选
            // 当g不是a[i]的公约数时,a[i]一定不可选
            if (a[i] % g == 0) {
                dp[i] = i > 1 ? std::max(dp[i - 1], dp[i - 2] + 1) : 1;
            } else {
                dp[i] = dp[i - 1];
            }
        }
        ans = std::max(ans, dp.back());
    }
    std::cout << ans << std::endl;
}

贪心的小C

题目描述: 小C在玩一个游戏。他手下有 n 名士兵,第 i 名士兵的能力值为 a[i]。

他需要将这 n 名士兵划分成 k 个 军团,满足:

  • 军团间的士兵编号互不重叠;
  • 每个军团至少包含一个士兵。

对第 j 个军团,其战力值m[j]为该军团内能力最低的士兵能力值 。

小C想要最大化所有军团战力值之和:

请你计算能够获得的最大总战力值 S。

输入输出

输入描述:

第一行输入两个整数 n 和 k (1≤k≤min(n,100),1≤n≤10^4),分别表示士兵数 n 和要划分的军团数 k。

第二行输入 n 个整数 a[1],a[2],...,a[n] (1≤a[i]≤100),表示每名士兵的能力值。

输出描述:

输出一个整数,表示将 n 名士兵划分成 k 个军团后,所有军团战力值之和的最大值。

测试样例

输入

复制代码
5 2
1 3 2 4 5

输出

复制代码
6

最优划分为军团{1,3,2,4}和军团{5};

两个军团的最小能力值分别为 1和5;

总能力值之和为 1+5 = 6。

输入

复制代码
5 3
5 1 2 4 3

输出

复制代码
10

最优划分为军团{5},{1,2,3},{4};

三个军团的最小能力值分别为 5,1,4;

总能力值之和为5+1+4=10。

超时解法

暴力枚举,必超时

c++ 复制代码
#include <bits/stdc++.h>
#include <numeric>

#define INF (std::numeric_limits<int>::max())

int ans = 0;

void Solve(const std::vector<int>& a, std::vector<int>& group_min_score, int pos) {
    if (pos == (int)a.size()) {
        int sum = 0;
        for (int score : group_min_score) {
            if (score == INF) return;
            sum += score;
        }
        ans = std::max(ans, sum);
        return;
    }
    
    // 尝试将pos位置的军人划分进各个军团
    for (int i = 0; i < group_min_score.size(); ++i) {
        int backup = group_min_score[i];
        group_min_score[i] = std::min(group_min_score[i], a[pos]);
        Solve(a, group_min_score, pos + 1);
        group_min_score[i] = backup;
    }
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);
    
    int n;  // 士兵数
    int k;  // 要划分的军团数
    std::cin >> n >> k;
    
    std::vector<int> a(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> a[i];
    }
    
    std::vector<int> group_min_score(k, INF);
    Solve(a, group_min_score, 0);
    
    std::cout << ans;
}

正确解法

大胆贪心即可

C++ 复制代码
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
	std::ios_base::sync_with_stdio(false);
	std::cin.tie(nullptr);

	int n, k;
	std::cin >> n >> k;

	std::vector<int> a(n);
	for (int i = 0; i < n; ++i) {
		std::cin >> a[i];
	}

	// 士兵按战斗力从大到小排序
	std::sort(a.begin(), a.end(), [](int x, int y) {
		return x > y;
	});

	int sum = 0;
	// 前k-1个士兵,每人各自成一个军团
	for (int i = 0; i < k - 1; ++i) {
		sum += a[i];
	}

	// 剩余的n-k个士兵,分到同一个军团,该军团的战斗力取他们中的最小值
	if (n - k > 0) {
		sum += a.back();
	}

	std::cout << sum << std::endl;
}

潜在同好

在小红书平台的社交推荐项目中,产品团队希望基于用户的日常行为习惯分数,挖掘潜在的"同好"关系。

系统简化如下,数据库中有 n 个用户的日常行为习惯分数,第 i 个用户的分数使用 a[i] 表示。记第 i 个用户和第 j 个用户构成"同好"关系,当且仅当 a[i] 能被 a[j] 整除,或者 a[j] 能被 a[i] 整除。

接下来将进行 m 次查询,每次给定一个额外的用户行为分数 x,请统计在数据库中,有多少不同的人能与这个人构成"同好"关系。

输入输出

输入描述:

第一行输入两个整数 n,m(1≤n,m≤5×10^5),表示数据库中用户数量、查询次数。

第二行输入 n 个整数 a[1],a[2],...,a[n] (1≤a[i]≤5×10^5),表示数据库中的用户日常行为习惯分数。

接下来 m 行,每行输入一个整数 x(1≤x≤5×10^5),表示一个额外的用户行为习惯分数。

输出描述:

对于每次查询,新起一行,输出一个整数,表示数据库中能与 x 构成"同好"关系的用户数量。

测试用例

输入

复制代码
5 3
1 2 2 5 6
4
2
1

输出

复制代码
3
4
5

超时答案

O(mn)

根据题意,n、m的上界均为MAX_VAL,因此时间复杂度的上界为O(MAX_VAL^2)

c++ 复制代码
#include <bits/stdc++.h>

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);
    
    int n;  // 用户数量
    int m;  // 查询次数
    std::cin >> n >> m;
    
    std::vector<int> a(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> a[i];
    }
        
    while (m--) {
        int x;
        std::cin >> x;
        
        int ans = 0;
        for (int num : a) {
            if (num % x == 0 || x % num == 0) {
                ++ans;
            }
        }
        
        std::cout << ans << std::endl;
    }
}

标准答案

不要每次遍历数组,用空间换时间。

C++ 复制代码
#include <iostream>
#include <vector>
#include <algorithm>

#define MAX_VAL ((int)5e5)

int main() {
	std::ios_base::sync_with_stdio(false);
	std::cin.tie(nullptr);

	int n, m;  // 用户数量,查询次数
	std::cin >> n >> m;

	std::vector<int> frequency(MAX_VAL + 1, 0);
	// 循环0
	// 时间复杂度O(n)
	for (int i = 0; i < n; ++i) {
		int num;
		std::cin >> num;
		++frequency[num];
	}

	// 打表,为可能输入的x预先算好输入数组中有多少个元素是它的倍数
	// 每确定一个i,内循环执行约MAX_VAL / i次
	// 嵌套循环执行总次数 = MAX_VAL / 1 + MAX_VAL / 2 + MAX_VAL / 3 + ... MAX_VAL / MAX_VAL = O(MAX_VAL * log(MAX_VAL))
	std::vector<int> multiple_count(MAX_VAL + 1, 0);
	for (int i = 1; i <= MAX_VAL; ++i) {
		for (int j = i * 2; j <= MAX_VAL; j += i) {
			multiple_count[i] += frequency[j];
		}
	}


	while (m--) {
		int x;
		std::cin >> x;

                // x有哪些倍数在frequency当中有统计?
		int ans = multiple_count[x];

		// x有哪些因子(包括它本身)在frequency当中有统计?
		// 时间复杂度O(sqrt(x)),上界为O(sqrt(MAX_VAL))
		for (int i = 1; i * i <= x; ++i) {
			if (x % i == 0) {
				ans += frequency[i];
				if (x / i != i) {
					ans += frequency[x / i];
				}
			}
		}

		std::cout << ans << std::endl;
	}
}

上述算法的时间复杂度n + MAX_VAL * log(MAX_VAL) + m * sqrt(MAX_VAL)。

根据题意,n、m的上界均为MAX_VAL,

因此时间复杂度的上界为MAX_VAL + MAX_VAL * log(MAX_VAL) + MAX_VAL * sqrt(MAX_VAL),等于O(MAX_VAL * sqrt(MAX_VAL)),远优于暴露解法的O(MAX_VAL^2)

相关推荐
2501_924534894 小时前
智慧零售商品识别误报率↓74%!陌讯多模态融合算法在自助结算场景的落地优化
大数据·人工智能·算法·计算机视觉·目标跟踪·视觉检测·零售
盖雅工场4 小时前
连锁零售排班难?自动排班系统来解决
大数据·人工智能·物联网·算法·零售
Greedy Alg4 小时前
LeetCode 438. 找到字符串中所有的字母异位词
算法·leetcode·职场和发展
Q741_1474 小时前
C++ 力扣 76.最小覆盖子串 题解 优选算法 滑动窗口 每日一题
c++·算法·leetcode·双指针·滑动窗口
AirMan9 小时前
深入揭秘 ConcurrentHashMap:JDK7 到 JDK8 并发优化的演进之路
后端·面试
lifallen9 小时前
Hadoop MapReduce 任务/输入数据 分片 InputSplit 解析
大数据·数据结构·hadoop·分布式·算法
熙xi.10 小时前
数据结构 -- 哈希表和内核链表
数据结构·算法·散列表
Ghost-Face10 小时前
并查集提高——种类并查集(反集)
算法