题目链接: Baby Ehab Partitions Again
2021.6.3 更新了一些关于最后删法的说明.
大致题意
给出一个长度为n的序列, 如果我们能把这个序列分成两部分, 使得两部分的元素和相等, 则认为序列是不好的.
要求: 删除尽可能少的数字, 使得序列变成好序列.
解题思路
01背包 + 思维
我们首先从整体的角度考虑一下, 如果这个序列本身就是好的, 有两种情况:
情况①: 序列的所有元素和为奇数, 那么我们一定凑不成相等的两部分.
情况②: 序列的所有元素和为偶数, 但是无法分成相等的两部分.
对于情况①而言, 我们直接统计元素和判断即可.
对于情况②而言, 假设所有元素和为sum, 我们需要判断能否凑出 sum/2. 若可以, 则序列一定不满足要求(不好的).
凑元素的过程: 对于一个元素而言, 我们只有选或者不选两种情况. (有没有感觉DNA起反应了? 是不是有01背包的感觉?)
我们需要用类似01背包的思路, 去看看当枚举到第i个元素时, 记录所有能凑出的元素和的情况.
当到这一步, 如果我们不能凑出sum/2, 输出0结束!
如果凑出了, 我们又如何删除元素呢?
比较简单的一个思路是, 如果我们使得删除后的序列, 变成情况①, 不就可以了?
那么已知我们当前的序列和是偶数了, 如果删除一个奇数元素, 一定成立.
但问题又来了, 如果没有奇数元素呢?
那说明所有的元素都是偶数, 我们给所有元素都除以2, 一定是和之前的选法一一对应的.
此时如果有奇数了, 我们删除奇数一定是成立的, 反之, 我们继续除2计算即可.
我们为什么多次除2之后, 序列删除此时的某个奇数一定成立? (下文的除2都指可以整除的情况)
我们可以反过来看, 首先明确, 除不除2, 是不影响序列的选择方式的.
假设我以当前的选择方式, 可以证明这个序列是不好的. 那么无论我把这个序列整体 乘2 或 除2, 它也都是不好的. 因为如果我保持以之前的方式去选择, 最后两边的加和仍是相同的.我们如果明白了这一点, 我们就可以认为原序列 与 原序列无数次除二后得到的新序列是完全等价的.
那么当我从新序列移除掉那个奇数元素, 可以证明新序列一定变成了好序列, 因为符合情况①, 因此新序列此时不存在一种选法, 使得可以证明新序列不是好序列.
因此得证: 我们移除原序列多次除2后的奇数数字, 一定可以使得原序列变好.
这里再给大家提供一种证明: 我们从位的角度去分析:
如果我们能把序列分成相等的两部分, 那么他们二进制的形式, 每一位上的1的个数都是相等的.
我们每次除2 或者 乘2, 并不会改变原本相等的1的数量, 只是可能将他们向左或向右移动了.
因此当我们无数次除2后, 移走此时最低位是1的数字(那个奇数), 此时一定会破坏这种平衡性.
如果你此时又有了想法, 那我拿别的位置的1可不可以呢? 答案是有可能也是可以的, 但是你要考虑到, 当我们拿走了某一个高位1后, 有可能会存在一种新的组合方式, 使得用相应个低位的1, 替代了这个高位的1.
AC代码
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 110, M = 2E3 + 10;
int a[N];
bool dp[M * N];
int main()
{
int n; cin >> n;
int sum = 0; //统计元素和
rep(i, n) scanf("%d", &a[i]), sum += a[i];
if (sum & 1) return printf("0\n"), 0; //情况①
dp[0] = 1; //显而易见, 什么都不选的情况, 即0是一定可以凑出的.
rep(i, n) {
for (int j = sum; j >= a[i]; --j) {
dp[j] |= dp[j - a[i]];
}
}
if (!dp[sum / 2]) return printf("0\n"), 0; //情况②
int d = 0; //求一下整个序列的gcd, 因为多次除二, 和除一次gcd是一样的效果.
rep(i, n) d = gcd(d, a[i]);
rep(i, n) if (a[i] / d % 2 == 1) return printf("1\n%d\n", i), 0;
return 0;
}