[开关问题]01串翻转全变为零 Apare_xzc
问题描述:
有一个01串,长度为len(1<=len<=20),我们可以对这个字符串进行翻转操作,定义如下:
我们可以对字符串任意位置进行操作,操作后,此位置与其相邻的两个位置的字符改变(‘1’变’0’,‘0’变’1’)。
问最少翻转多少次这个字符串可以全部变为为’0’。输出最少步数,如果无法实现,输出NO
输入描述:
多组输入,第一行一个正整数T(1<=T<=100), 下面有T行,每行一个01字符串(长度<=20,可能为全零串)。
输出要求:
每个输入输出一行,输出最少步数,如果无法实现,输出”NO“(不带引号)
样例输入:
7
0
1
00
01
10
11
111
样例输出:
0
1
0
NO
NO
1
1
(阿里2020笔试题)
分析:
我们先手动计算出一些串的答案,粗略地探究一下其中的规律:
输入:0
答案:0
分析:不需要翻转
输入:1
答案:1
分析:翻转第一位即可。
1 ---flip(1)---> 0
输入:00
答案:0
分析:不需要翻转
输入:01
答案:NO
分析:无论翻转哪一位,都只会是01和10两个状态,所以是NO。
输入:11
答案:1
分析:只需要翻转第一位或第二位即可。
11 ---flip(1) or flip(2) ---> 00
输入:000
答案:0
分析:不需要翻转
输入:001:
答案:2
分析:001 --- flip(1) ---> 111 --- flip(2) ---> 000
输入:010
答案:3
分析:可以先把1移到最左边,转化为100
010 ---flip(1)---> 100 --->flip(2)---> 011 ---> flip(3) ---> 000
输入:011:
答案:1
分析:直接翻转第三位即可
011 ---flip(3)---> 000
输入:100:
答案:2
分析:同001,可以先翻转第3位变为三个1,再翻转第2位。亦可以先翻转第二位变为011,然后再翻转第三位
100 ---flip(2)---> 011 ---flip(3)---> 000
100 ---flip(3)---> 111 ---flip(2)---> 000
输入:101
答案:2
分析:先翻转第一位变为011
101 ---flip(1)---> 011 ---flip(3) ---> 000
输入:110
答案:1
分析:同011,直接翻转第一位即可
110 ---flip(1)---> 000
输入:111
答案:1
分析:直接翻转第2位即可
111 ---flip(2)---> 000
输入:0000
答案:0
分析:不需要翻转
输入:0001
答案:3
分析:先翻转1,再翻转2,可以把1向后传递
0001 ---flip(1)---> 1101 ---flip(2)---> 0011 ---flip(4)---> 0000
输入:1000
答案:3
分析:可以做0001的镜像,也可以如下
1000 ---flip(1)---> 0100 ---flip(3)---> 0011 ---flip(4)---> 0000
1000 ---flip(4)---> 1011 ---flip(3)---> 1100 ---flip(1)---> 0000
输入:0010
答案:2
分析:先翻转第1位,然后变为3连串
0010 ---flip(1)---> 1110 ----flip(2)---> 0000
输入:10000
答案: NO
分析:穷举后发现无法全部变为零
输入:100000
答案:4
分析:先翻转第一位,让1向后移动。然后每次1都向后移动,最后变为00...0011的状态
100000 ---flip(2)---> 011000 ---flip(3)---> 000100 ---flip(5)
---> 000011 ---flip(6)---> 000000
输入:1000,000
答案:5
分析:
1000,000 ---flip(1)---> 0100,000 ---flip(3)---> 0011,000 ---flip(5)
--->0000,100 ---flip(6)---> 0000,011 ---flip(7)---> 0000,000
输入:10,000,000
答案:NO
分析:无论先翻转第一位还是先翻转第二位都不行
10,000,000 ---flip(2)---> 01,100,000 ---flip(3)---> 00,010,000 --->flip(5)
--->00,001,100 ---flip(7)---> 00,000,010 ---flip(8)---> 00,000,001
规律总结与分析:
- 左右镜像对称的字符串的答案相同
- 并不是所有的串都可以翻转为全零串
- 似乎所有的串若可以变为全零串,则存在升序的翻转序列
- 形如
1000...00
的串,如果0的个数对3取模后余数为1,则无法还原。若余数为0,先翻转第1位即可。若余数为2,先翻转第2位即可。 - 我们考虑是否可以贪心。为了保证无后效性,我们可以从左往右进行操作。如果s[i]为1,那么我们就翻转i+1,这样就不会影响到前面已经全为零的串。但是第一个位置很特殊,因为翻转第一个位置,不会影响前面的字符(因为它本来就是第一个), 所以若s[1]为1,我们可以翻转第1位,亦可以翻转第2位。我们可以看如下的例子:
110 ---flip(1)---> 000
111 ---flip(2)---> 000
101 ---flip(1)---> 011 ---flip(3)---> 000
001 ---flip(1)---> 111 ---flip(3) --->000
100 ---flip(2)---> 011 ---flip(3)--->000
我们可以看到,无论第1位是’0’还是’1’, 在有些情况下,我们需要最先翻转第1位,有些情况下,我们需要最先翻转第2位。我们不好做出判断。我们不如这两种策略都试一次,取最优的。
6. 我猜想,步数最多的状况就是形如:1000...000
,因为我们要让这单个的1从前往后一个接一个传递,直到转化成形如0000..0011
的情况,才可以翻转最后一位变零。我们可以分析一下。
7. 至此,我们的贪心策略出炉了:
- 先flip(1),然后令i从第1位至倒数第二位遍历,若该位为
1
,则flip(i+1), 若最后剩下00...00001
,则不行。 - 然后不flip(1),零i从第1为至倒数第二位遍历,最后先从第2位开始翻转,若最后剩下
00...0001
,则不行。 - 两次取最少的步数,若两次都没有还原成功,则无法实现,输出NO。
我们可以用这个贪心算法大概计算一下长度小于等于20的字符串最大的翻转次数。由第7点描述的算法可知,我们从左向右处理,遇到0就直接往后跳。如果有连续的1,我们一下子就可以消去三个。所以,步数最多的情况可能为1000...00
的形式。这种形式在翻转的过程中,相当于1向后移动,而且字符串中1的个数依次变化。1个->2个->1个->2个...
示例如下:
6个零,5步
1000,000 --->
0100,000 --->
0011,000 --->
0000,100 --->
0000,011 --->
0000,000
8个零,6步
100,000,000 --->
011,000,000 --->
000,100,000 --->
000,011,000 --->
000,000,100 --->
000,000,011 --->
000,000,000
根据我们之前的规律,len-1为后面零的个数。
- 若
(len-1)%3==1
则无解。 - 若
(len-1)%3==0
,我们先翻转第1位,则字符串中1的规律为:1 -> 11 -> 1 -> 11
,一共经历floor((len-1)/3)
个周期,每个周期长度为2,步数为floor((len-1)/3) * 2, 最后还要加一步翻转最后一位,使得00…0011变为全零,此时的步数为:(len-1)/3*2 + ((len-1)%3==0)
- 若
(len-1)%3==2
,我们先翻转第2位,则字符串中1的规律为:11 -> 1->11->1
, 一共经历floor((len-1)/3)
个周期,每个周期长度为2,步数为(len-1)/3 * 2
- 所以有解的形如
1000...00
的串在此贪心策略下的最少步数为:(len-1)/3*2 + ((len-1)%3==0)
- 1000…0(18个零)的最少步数为:
18/3*2+(18%3==0) = 6*2+1 = 13
- 1000…0(19个零)因为
19%3==1
,所以无解 - 1000…0(17个零)的最少步数为:
17/3*2 = 10
- 所以我们猜测可能长度为20的字符串在有解的情况下最少步数的上限可能为13(或者稍微比13大一些)
- 至此我们可以写出上述贪心策略的代码了:
贪心策略代码:
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
void Flip(string& str,int p) //翻转字符串
{
for(int i=p-1;i<=p+1;++i)
if(i>=0&&i<str.length())
str[i] = (str[i]=='0')?'1':'0';
}
int cal(string s) {
string tmp = s;
int len = s.length();
int cnt = 0,ans=-1;
//翻第一个
Flip(s,0),cnt=1;
for(int i=0;i<len-1;++i)
{
if(s[i]=='0') continue;
Flip(s,i+1);++cnt;
}
if(s[len-1]!='0') ans = INF;
else ans = cnt;
//不翻第一个
cnt = 0;
s = tmp;
for(int i=0;i<len-1;++i)
{
if(s[i]=='0') continue;
Flip(s,i+1);++cnt;
}
if(s[len-1]=='1') cnt = INF;
ans = min(ans,cnt);
return ans;
}
int main(void) {
// freopen("in0.txt","r",stdin);
// freopen("tanxin.txt","w",stdout);
int T;
cin>>T;
while(T--)
{
string s;
cin>>s;
int res = cal(s);
if(res>1000) puts("NO");
else cout<<res<<endl;
}
return 0;
}
我们可以暴力bfs搜索,然后和贪心对拍一下:
bfs代码如下:
#include <bits/stdc++.h>
using namespace std;
int len;
unordered_map<string,int> mp;
void rev(string& s,int p)
{
for(int i=p-1;i<=p+1;++i)
{
if(i>=0&&i<len)
{
if(s[i]=='1') s[i] = '0';
else s[i] = '1';
}
}
}
int bfs(string st,string ed)
{
mp.clear();
mp[st] = 0;
queue<string> Q;
Q.push(st);
string now,to;
while(!Q.empty())
{
now = Q.front();Q.pop();
to = now;
if(now==ed) return mp[now];
for(int i=0;i<len;++i) //判断每一位
{
for(int p=i-1;p<=i+1;++p)
{
if(p<0||p>=len) continue;
rev(to,p);
if(to==ed)
{
return mp[now]+1;
}
if(mp[to]) //搜过了
{
rev(to,p);continue;
}
Q.push(to); mp[to] = mp[now]+1;
rev(to,p);
}
}
}
return -1;
}
int main(void)
{
// freopen("in0.txt","r",stdin);
// freopen("bfsout.txt","w",stdout);
int T;
string st;
cin>>T;
while(T--)
{
cin>>st;
len = st.length();
string ed;
for(int i=0;i<len;++i) ed+='0';
if(st==ed)
{
puts("0");continue;
}
int ans = bfs(st,ed);
if(ans==-1){
puts("NO");
continue;
}
cout<<ans<<endl;
}
return 0;
}
我们中规中矩地BFS计算了几百个小数据之后,发现和贪心的输出是一致的,证明我们的贪心策略应该问题不大。我们来算算BFS的复杂度。
刚才我们得到的结论是答案的的最大值为13。BFS的最坏时间复杂度就是20^13
,这不太能接受。一个大数据都跑不完的。
我们可以从还原态全零字符串开始BFS,求一下20位的串,答案的上限是不是13。我们这次用bitset维护01串。
从000...00
(20个零)BFS打表代码:
#include <bits/stdc++.h>
using namespace std;
char a[20];
int r[1<<20];
int len,step;
unordered_map<string,int> mp;
void bfs(int sz)
{
bitset<20> st,now,to;
queue<bitset<20> > Q;
Q.push(st); //入队之前标记
r[st.to_ulong()] = 0;
while(!Q.empty())
{
now = Q.front();
Q.pop();
for(int i=0;i<sz;++i)
{
to = now;
to.flip(i);
if(i-1>=0) to.flip(i-1);
if(i+1<sz) to.flip(i+1);
int idx = to.to_ulong();
if(r[idx]!=-1) continue;
r[idx] = r[now.to_ulong()]+1;
step = r[idx];
Q.push(to);
}
}
}
int main(void) {
// freopen("out.txt","w",stdout);
memset(r,-1,sizeof(r));
r[0] = 0;
int M = 1<<20;
bfs(20);
cout<<"step = "<<step<<endl;
for(int i=0;i<M;++i)
{
bitset<20> bt = i;
cout<<bt<<" ";
cout<<r[i]<<endl;
}
return 0;
}
我们打表之后,发现20位的字符串,答案上限的确是13。
双向BFS的复杂度为:2*20^(13/2)
,这个勉强可以跑出来:
双向BFS代码:
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
int len;
void Flip(string& s,int p) {
for(int i=p-1;i<=p+1;++i)
if(i>=0&&i<len)
s[i] = (s[i]=='0')?'1':'0';
}
int two_dir_bfs(string st,string ed)
{
if(st==ed) return 0;
unordered_map<string,int> mp,mp2;
queue<string> Q,Q2;
mp[st] = 0;
mp2[ed] = 0;
string now,to;
Q.push(st); Q2.push(ed);
while(Q.size()>0||Q2.size()>0) {
if(Q.size()<=Q2.size()&&!Q.empty()) {
now = Q.front();Q.pop();
for(int i=0;i<len;++i) {
to = now;
Flip(to,i);
if(mp2.find(to)!=mp2.end()) return mp2[to]+mp[now]+1;
if(mp.find(to)!=mp.end()) continue;
Q.push(to); mp[to] = mp[now]+1;
}
} else {
now = Q2.front();Q2.pop();
for(int i=len-1;i>=0;--i) {
to = now;
Flip(to,i);
if(mp.find(to)!=mp.end()) return mp[to]+mp2[now]+1;
if(mp2.find(to)!=mp2.end()) continue;
Q2.push(to); mp2[to] = mp2[now]+1;
}
}
}
return INF;
}
unordered_map<string,int> mp;
int main(void) {
// freopen("in0.txt","r",stdin);
// freopen("two_dir_bfs_out.txt","w",stdout);
int T;cin>>T;
while(T--)
{
string st,ed;
cin>>st;
len = st.length();
for(int i=0;i<len;++i) ed +='0';
int ans = two_dir_bfs(st,ed);
if(ans>100) puts("NO");
else printf("%d\n",ans);
}
return 0;
}
双向BFS显然快很多,但还是可能超时。
我们都暴力搜索了,不如再试试IDA算法。虽然这里不适合用IDA,因为有的字符串是无解的,但是我们刚才贪心分析也好,BFS打表也罢,得到了上限为13,大约为(len-1)/32+1。我们可以来一发IDA,我们限制最大搜索深度为(len-1)/32+1, 然后跑IDA算法即可。我们深度从0开始递增,步长为1,第一次搜到目标态的深度就是我们的答案。
IDA*搜索代码如下:
#include <bits/stdc++.h>
using namespace std;
char a[100];
int len,ok,restep;
void rev(int pos)
{
for(int i=pos-1;i<=pos+1;++i)
if(i>=0&&i<len) a[i] = (a[i]=='0')?'1':'0';
}
void dfs(int step,int pre) {
if(ok) return;
bool flag = true;
for(int i=0;i<len;++i) {
if(a[i]=='1') {
flag = false;break;
}
}
if(flag) {
ok = true;return;
}
//
if(step==restep) return;
for(int i=0;i<len;++i)
{
if(i==pre) continue;
if(i-1>=0&&i+1<len&&a[i]=='0'&&a[i+1]=='0'&&a[i-1]=='0') continue;
rev(i);
dfs(step+1,i);
rev(i);
}
}
int main(void) {
// freopen("in0.txt","r",stdin);
// freopen("idastar.out","w",stdout);
int T;
scanf("%d",&T);
while(T--)
{
scanf("%s",a);
len = strlen(a);
ok = false;
for(int i=0;i<=13&&i<=len+1;++i)
{
restep = i;
dfs(0,-1);
if(ok) break;
}
if(!ok) puts("NO");
else printf("%d\n",restep);
}
return 0;
}
虽然IDA* 同时有dfs和bfs的好处,但是奈何最大深度13太大。小数据还是跑的飞快的,字符串答案大了的话,比BFS也好不到哪里去。但终归是一种方法吧。
综上,还是贪心复杂度最低(废话),O(n)。
数据生成代码:
#include <bits/stdc++.h>
using namespace std;
int len;
char a[30];
void dfs(int x)
{
if(x==len)
{
a[len] = 0;
puts(a);
return;
}
a[x] = '0';
dfs(x+1);
a[x] = '1';
dfs(x+1);
}
int main(void) {
freopen("in0.txt","w",stdout);
for(int i=1;i<=20;++i)
{
len = i;
dfs(0);
}
return 0;
}
对拍比较程序代码:
#include <bits/stdc++.h>
using namespace std;
string f1 = "idastarout.txt";
string f2 = "tanxin.txt";
int main(void) {
fstream in1,in2;
in1.open(f1);
in2.open(f2);
string a,b;
int cnt = 0;
int cerr = 0;
while(getline(in1,a))
{
++cnt;
getline(in2,b);
if(a!=b)
{
++cerr;
cout<<"On line "<<cnt<<": "<<a<<" | "<<b<<endl;
}
}
cout<<"不同的地方有:"<<cerr<<"个"<<endl;
return 0;
}
xzc
2020.4.3
1:08