1. 程式人生 > 其它 >FZOJ#4897 灰燼(搜尋剪枝)

FZOJ#4897 灰燼(搜尋剪枝)

題面

給你一個長度為\(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;
}