[開關問題]01串翻轉全變零 阿里筆試2020 Apare_xzc

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

規律總結與分析:

  1. 左右鏡像對稱的字符串的答案相同
  2. 並不是所有的串都可以翻轉爲全零串
  3. 似乎所有的串若可以變爲全零串,則存在升序的翻轉序列
  4. 形如1000...00的串,如果0的個數對3取模後餘數爲1,則無法還原。若餘數爲0,先翻轉第1位即可。若餘數爲2,先翻轉第2位即可。
  5. 我們考慮是否可以貪心。爲了保證無後效性,我們可以從左往右進行操作。如果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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章