【题解】FZOJ4897 灰烬

题面

给你一个长度为\(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;
}

posted @ 2021-09-19 21:35  hzy1  阅读(39)  评论(0编辑  收藏  举报