[開關問題]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