【划分型贪心+因式分解】codeforces 1992 F. Valuable Cards
题目
https://codeforces.com/contest/1992/problem/F
题意
输入一个正整数 \(T(1 \leq T \leq 10^3)\),表示一共 \(T\) 组测试用例,保证所有测试用例的 \(n\) 之和不超过 \(10^5\)。对于每个测试用例:
第一行输入两个正整数 \(n\) 和 \(x(1 \leq n \leq 10^5,2 \leq x \leq 10^5)\);
第二行输入一个长为 \(n\) 的整数数组 \(a\),其中 \(a_i(1 \leq a_i \leq 2 \times 10^5, a_i != x)\) 代表第 \(i\) 个数的值。
若一个连续的子数组中,存在一个或以上的子序列的乘积为 \(x\),则称该子数组为坏段。问最少将数组 \(a\) 分为多少个不重叠的子数组,可使得每个子数组都不是坏段。输出最少需要分段的数量。
题解
很容易想到的一个划分型贪心思路是:从左往右遍历数组,当从下标位置 \(i\) 遍历到下标位置 \(j(j > i)\) 时,若子数组 \(a_{i..j}\) 已经存在一个子序列的乘积为 \(x\),但从下标位置 \(i\) 遍历到下标位置 \(j-1\) 时,子数组 \(a_{i..(j-1)}\) 还不存在一个子序列的乘积为 \(x\),那么需要在下标位置 \((j-1)\) 与下标位置 \(j\) 之间明显需要隔开。然后从下标位置 \(j\) 重复上述思路,直至遍历完数组。
于是,问题就转化为如何快速判断子数组 \(a_{i..j}\) 是否存在一个子序列的乘积为 \(x\)。
不妨假设 \(x = p \times q \times r\) 是 \(x\) 的一种因式分解的方式,那么显然成立的是:
- \(p \text{ | } x\)
- \(q \text{ | } x\)
- \(r \text{ | } x\)
亦即,若子数组 \(a_{i..j}\) 中存在一个子序列的乘积为 \(x\),那么这个子序列的每个元素都必定是 \(x\) 的因子。那么,若数组元素不是 \(x\) 的因子则可以直接跳过;若数组元素是 \(x\) 的因子,则需要进行以下思考:
不妨仍以 \(x = p \times q \times r\) 为例进行讲解:
首先要理清一个思路,为什么当从下标位置 \((j-1)\) 遍历到下标位置 \(j\) 时就需要隔开了?
理由就是因为在引入 \(a_j\) 以后,就存在了至少一个包含元素 \(a_j\) 的子序列的乘积为 \(x\)。
不妨假设当前遇到的数组元素 \(a_j\) 的值就是 \(p\),那么这意味着:
1.\(a_j \text{ | } x\),即 \(p \text{ | } x\)
2.在子数组 \(a_{i..(j-1)}\) 中已经存在至少一个子序列的乘积为 \(\frac{x}{a_j}\)
因此,我们可以得到的一个基本思路是:判断引入 \(a_j\) 以后,再碰到什么数即可凑出 \(x\),这可以用一个集合 \(S\) 维护。那么在数组遍历过程中,若元素 \(a_j\) 不是 \(x\) 的因子,则直接跳过,否则判断集合 \(S\) 中是否有 \(a_j\),若有则直接判定当前位置需要隔开,否则就用集合 \(S\) 中的每个数除以 \(a_j\),若结果大于 \(0\),可以加入到集合 \(S\) 中(需要注意的是,要在遍历完集合以后再加入,否则会引发 ub)。
参考代码
方法一:使用两个 set 集合维护(编码简单,效率较低)
#include<bits/stdc++.h>
using namespace std;
int T, n, x;
int a[200005];
void solve() {
int ans = 1;
cin >> n >> x;
set<int> st1 = {x}, st2;
for (int i = 0; i < n; ++ i) {
cin >> a[i];
if (x % a[i]) continue;
if (st1.find(a[i]) != st1.end()) {
++ ans;
st1 = {x, x / a[i]};
} else {
for (auto &it: st1) if (it % a[i] == 0) st2.insert(it / a[i]);
st1.merge(st2);
st2.clear();
}
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
cin >> T;
while (T --) solve();
return 0;
}
方法二:栈(编码略微复杂,效率高)
#include<bits/stdc++.h>
using namespace std;
constexpr int N = 200005;
int T, n, x;
int a[N];
int d[N];
bool vis[N];
void solve() {
int ans = 1, m = 0;
cin >> n >> x;
stack<int> stk;
vis[x] = true;
d[m ++] = x;
for (int i = 0; i < n; ++ i) {
cin >> a[i];
if (x % a[i]) continue;
if (vis[a[i]]) {
++ ans;
m = 1;
while (!stk.empty()) vis[stk.top()] = false, stk.pop();
}
for (int j = 0, k = m; j < k; ++ j)
if (d[j] % a[i] == 0 && !vis[d[j] / a[i]]) {
d[m ++] = d[j] / a[i];
vis[d[j] / a[i]] = true;
stk.push(d[j] / a[i]);
}
}
while (!stk.empty()) vis[stk.top()] = false, stk.pop();
vis[x] = false;
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
cin >> T;
while (T --) solve();
return 0;
}
浙公网安备 33010602011771号