題目描述:
題目分析:
將操作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]);
}