行为权重
小红是小红书的用户行为分析师。平台将每次用户行为映射为一个正整数权重序列 {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)