牛客小白月赛112——E智乃的“凑数”题(Easy Version & Hard Version)
题目
智乃的“凑数”题(Easy Version)
智乃的“凑数”题(Hard Version)
题解(Easy版)
题目分析
对于每个行单元格和列单元格的数字相乘再相加,我们可以对这个公式进行化简,最后得到的就是 行和\(\times\)列和。那么题目就相当于要求在一个给定的整数数组 ( \(k_1\), \(k_2\), ..., \(k_n\) ) 中,找出一个元素子集,将其划分成两个部分,使得两部分的元素和分别形成行和和列和,使得它们的乘积等于一个查询值x。如果存在这样的划分,则输出其中一种方案,否则输出 "No"。
换句话说,我们需要判断能否将数组元素划分为两部分,使得:
- 其中一部分的元素和为 a,另一部分的元素和为 b,且a \(\times\) b = x。
- 我们只需要找到一种可行的划分方案。
解题思路
本题本质上是一个 二维的 0/1 背包问题:
- 令
f[x][y]表示是否能通过某些元素的组合形成x和y。 - 递推时,如果
f[x-k[i]][y]为真,则f[x][y]也为真;同理,如果f[x][y-k[i]]为真,则f[x][y]也为真。 - 我们还需要使用
pre[x][y]记录转移来源,以便回溯输出具体的方案。
状态转移
- 初始化
f[0][0] = true,表示空集合可以形成 0,0 这个状态。 - 枚举每个元素
k[i],尝试将它放入两个集合之一:- 如果
x >= k[i]并且f[x-k[i]][y] = true,则f[x][y] = true,且pre[x][y] = k[i],表示x由x-k[i]变过来。 - 如果
y >= k[i]并且f[x][y-k[i]] = true,则f[x][y] = true,且pre[x][y] = -k[i],表示y由y-k[i]变过来。
- 如果
查询部分
- 对于每个查询值
x,我们要找到两个正整数a, b使得a × b = x。 - 枚举
a,让b = x / a,并检查f[a][b]是否为true。 - 如果存在
f[a][b],则回溯输出a和b是如何由数组k组合得到的。
复杂度分析
- 预处理
f[x][y]使用 二维背包动态规划,时间复杂度为 O(n \(\times\) 100 \(\times\) 100)。 - 每次查询需要遍历
sqrt(x)个因子,每个因子最多回溯n次,时间复杂度为 O(\(\sqrt{x}\) \(\times\) n)。 - 由于
n, m <= 100,最坏情况下可达 O(106),是可以接受的。
总结
- 该题本质上是 0/1 背包问题的变形,需要使用 二维动态规划 记录可行的
(a, b)组合。 - 预处理
f[x][y]后,可以 快速查询 是否存在符合a × b = x的划分方案,并 回溯输出方案。 - 在n,m <= 100范围内能高效运行,但是对于hard就不能运行了。
参考代码(Easy版)
#include<iostream>
#include<cmath>
#include<vector>
using namespace std;
const int N = 110;
int n, m;
int k[N],pre[N][N];
bool f[N][N];
void query(int n){
for(int i = 1; i <= sqrt(n); i ++){
if(n % i) continue;
int x = i, y = n / i;
if(f[x][y]){
puts("Yes");
vector<int> vx,vy;
while(x || y){
if(pre[x][y] > 0){
vx.emplace_back(pre[x][y]);
x -= pre[x][y];
}else{
vy.emplace_back(-pre[x][y]);
y += pre[x][y];
}
}
cout << vx.size() << " " << vy.size() << endl;
for(auto x : vx) cout << x << " ";
puts("");
for(auto y : vy) cout << y << " ";
puts("");
return;
}
}
puts("No");
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> k[i];
f[0][0] = true;
for(int i = 1; i <= n; i ++){
for(int x = 100; x >= 0; x --){
for(int y = 100; y >= 0; y --){
if(f[x][y]) continue;
if(x >= k[i] && f[x - k[i]][y]){
f[x][y] = true;
pre[x][y] = k[i];
}else if(y >= k[i] && f[x][y - k[i]]){
f[x][y] = true;
pre[x][y] = -k[i];
}
}
}
}
while(m --){
int x;
cin >> x;
query(x);
}
return 0;
}
题解(Hard版)
hard中数据较大,可以使用一维去映射二维数组,那样子空间只要二维的一半,牛客的空间优化较好,二维状态下也能过。
参考代码(Hard版)
#include<iostream>
#include<cmath>
#include<vector>
using namespace std;
const int N = 10010;
int n, m;
int k[N],pre[N][N];
bool f[N][N];
void query(int n){
for(int i = 1; i <= sqrt(n); i ++){
if(n % i) continue;
int x = i, y = n / i;
if(f[x][y]){
puts("Yes");
vector<int> vx,vy;
while(x || y){
if(pre[x][y] > 0){
vx.emplace_back(pre[x][y]);
x -= pre[x][y];
}else{
vy.emplace_back(-pre[x][y]);
y += pre[x][y];
}
}
cout << vx.size() << " " << vy.size() << endl;
for(auto x : vx) cout << x << " ";
puts("");
for(auto y : vy) cout << y << " ";
puts("");
return;
}
}
puts("No");
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> k[i];
f[0][0] = true;
for(int i = 1; i <= n; i ++){
for(int x = 10000; x >= 0; x --){
for(int y = (x?10000/x:10000); y >= 0; y --){
if(f[x][y]) continue;
if(x >= k[i] && f[x - k[i]][y]){
f[x][y] = true;
pre[x][y] = k[i];
}else if(y >= k[i] && f[x][y - k[i]]){
f[x][y] = true;
pre[x][y] = -k[i];
}
}
}
}
while(m --){
int x;
cin >> x;
query(x);
}
return 0;
}

浙公网安备 33010602011771号