题目描述:
题目分析:
将操作2看做一条边,如果最后的方案连出了一个环,那么将这个环上所有的边换成操作1不会更劣。
所以最优方案一定是操作二连成森林,然后剩下的点用操作一。
因为一棵树可以减少1次操作,所以我们想要分出尽量多的连通块。
考虑怎么判断集合能否使用操作二清零:
随便选一个点作根,假设每条边两端减掉的值都是,从叶子开始依次减,那么减到根的时候将根此时的权值用变量表示,那么与根深度奇偶性相同的点的变量带的符号就是正号,相异的就是负号:
我们希望根节点的值为0,那么就是 奇数深度节点的权值和 = 偶数深度节点的权值和。
然后考虑每条边可以给两端的任意一端+1,总共有条边可以用,那么集合可以用操作二清零的条件就是可以将分为两个非空集合,满足
判断一个集合是否满足上面的条件,可以,但是太慢。问题相当于是将中的数带上正负号,考虑折半然后合并。设S的左半部分的所有状态为,右半部分的所有状态为,因为只需要判断是否存在解,所以先将和分别排序(这个可以在求的时候排好),然后设一个指针指向的最右端,指向的最左端,如果,那么,知道满足条件后,循环判断是否满足。(这一部分结合代码理解一下)
这样对于一个状态,的复杂度就是,求个和就是官方题解中的
然后考虑怎么求答案,官方题解的做法是:如果可行,那么令,然后对做次子集卷积,如果中存在某一位不为0,说明存在一种分个集合的方案,于是要做的就是找到最小的满足中所有位都为0,最终的答案就是。
求这个可以考虑倍增,如果超过了就退,这样复杂度是的,还有点常数。
这样做显然比较麻烦,我们考虑暴力的子集卷积是的,但是其实完全没有必要,对于一个由多个可行集合组成的,我们只需要在它最小的那个可行子集处更新它。所以只有当集合是可行集合,且不能由其它可行集合组合而成时,我们用它去更新。这样剪枝之后虽然复杂度没有什么保证,但是实际运行效果非常好,Codeforces上目前最快的写法大都是这样写的。
Code:
#include<bits/stdc++.h>
#define maxn 20
#define LL long long
using namespace std;
const int N = 1<<20|5;
int n,f[N];
LL a[maxn];
LL b[maxn],sl[N],sr[N];
void getque(LL *s,int l,int r){
int m=1; s[1]=0;
static LL pos[N],neg[N];
for(int i=l;i<=r;i++,m<<=1){
for(int j=1;j<=m;j++) pos[j]=s[j]+b[i],neg[j]=s[j]-b[i];
for(int x=1,y=1,k=1;k<=m<<1;k++)
s[k]=x>m?neg[y++]:y>m?pos[x++]:pos[x]<neg[y]?pos[x++]:neg[y++];
}
}
bool check(int S){
int sz=0; LL sum=0;
for(int i=1;i<=n;i++) if(S>>i-1&1) sum+=a[i],b[++sz]=a[i];
if(sum-(sz-1)&1) return 0;
getque(sl,1,sz/2),getque(sr,sz/2+1,sz);
int L=1<<(sz/2),R=1<<(sz-sz/2),need=1+(abs(sum)<sz)*2;
for(int i=R,j=1;i>=1;i--){
while(j<=L&&sl[j]+sr[i]<=-sz) j++;
for(int k=j;k<=L&&need&&sl[k]+sr[i]<sz;k++) need--;
}
return !need;
}
int main()
{
scanf("%d",&n); int m=0;
for(int i=1;i<=n;i++) scanf("%lld",&a[i]),a[i]&&(a[++m]=a[i]);
n=m; int all=(1<<n)-1;
for(int s=1;s<=all;s++) if(!f[s]&&check(s)){//!f[s] can cut tons of situations.
int r=all^s; f[s]=1;
for(int t=r;t;--t&=r) f[s|t]=max(f[s|t],f[t]+1);
}
printf("%d\n",n-f[all]);
}