[开关问题]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