【题解】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;
}