FZOJ#4897 灰燼(搜尋剪枝)
阿新 • • 發佈:2021-09-20
題面
給你一個長度為\(n(n\le 40)\)的\(01\)序列,每次可以任意選擇一個\(k(1\le k\lt n)\),然後將前\(n-k\)個數字複製一份,將複製的一份向後移動\(k\)格並與原位置上的數字取並。求最少幾次讓整個序列全為\(1\)。
題解
首先你肯定想到了每一步都去列舉\(k\),然後取貢獻最大的\(k\)來更新序列
但是是錯的qwq(97pts, sad story...)
問題就在於本質上是個貪心。而資料範圍一看就是要搜尋。
如果直接暴力搜的話,大概長這樣
bool check(ll S) {//判斷狀態S是不是全為1 return !(S&(S+1)); } void DFS(int now,ll S) { if(now>6) return; if(check(S)) { ans=min(ans,now); return; } for(int k=1; k<=n; ++k) { DFS(now+1,S|(S>>k),k);//列舉k,搜尋下一個狀態 } }
接下來介紹幾種剪枝思路
1. 先計算出答案的邊界,ans最大為\(log_2^n\)向上取整,代入資料是\(6\)。
也就是說,當搜尋到第\(ans-1\)層時仍然沒有更新答案,就可以直接返回;然後再初始化ans=6即可。
事實上,第\(6\)層的狀態數比前\(5\)層的還要多,所以加了這個剪枝就可以過了。
2. 發現對於兩次連續的移動\(k1,k2\),交換他們的順序,變為\(k2,k1\),對序列的改變是完全一樣的。
那麼我們就可以規定\(k\)單調遞增,然後每次的\(k\)直接從上一次的\(k\)開始列舉。
這個剪枝的效果也很顯著,只加這個優化也可以通過。
程式碼:
#include<bits/stdc++.h> using namespace std; using ll=long long; const int N=45; int n,ans; bool check(ll S) { return !(S&(S+1)); } void DFS(int now,ll S,int lastk) { if(check(S)) { ans=min(ans,now); return; } if(now+1>=ans) return; for(int k=lastk; k<=n; ++k) { DFS(now+1,S|(S>>k),k); } } signed main() { ios::sync_with_stdio(false); cin.tie(0); int T; cin>>T; while(T--) { string s; s.clear(); cin>>s; n=s.size(); if(s[0]=='0') { cout<<"-1\n"; continue; } ll S=0; for(int i=0; i<n; ++i) { S=(S<<1)|(s[i]-'0'); } ans=6; DFS(0,S,1); cout<<ans<<"\n"; } return 0; }