1. 程式人生 > >2018南京大學計算機夏令營機試

2018南京大學計算機夏令營機試

1. Count number of binary strings without consecutive 1’s

Given a positive integer n(3≤n≤90), count all possible distinct binary strings of length n such that there are no consecutive 1's .

Examples:

  Input:  2 
  Output: 3 // The 3 strings are 00, 01, 10 
  ​
  Input: 3 
  Output: 5 // The 5 strings are 000, 001, 010, 100, 101

中文題意:給定一個正整數n(3≤n≤90),數出長度為n的所有可能的不同二進位制串的個數,使得串中沒有連續的1出現。

解題思路:考試時第一想法是用回溯法,因為長度為n的所有二進位制串形成一棵子集樹,所以可以用DFS遍歷子集樹,遍歷過程中檢查是否有連續的1出現,作為約束函式用於剪枝,時間複雜度為O(2^n)。然而用程式碼實現以後發現10個測點只通過了5個,還有5個測點超時了,畢竟時間複雜度O(2^n)實在是太高了。

​       思考了一會兒,忽然想起來以前學習數理邏輯與圖論這門課的時候在遞推那一章做過這道題,於是恍然大悟,可以用動態規劃求解!令a[i]為長度為i的不含連續1的二進位制串的個數,考慮長度為i的不含連續1的任意一個二進位制串:若第i位(末位)為0,則第i-1位可以為0也可以為1,這種情況的二進位制串有a[i-1]個;若第i位為1,則第i-1位只能為0(否則最後兩位為連續兩個1,不符題意),進一步考慮第i-2位,由第i-1位為0可知第i-2位可以為0也可以為1,這種情況的二進位制串有a[i-2]個。綜上可以寫出遞推式 a[i]=a[i-1]+a[i-2], i≥3,邊界條件為a[1]=1,a[2]=3。本題動態規劃的時間複雜度為O(n),所以得到一個教訓——能用動態規劃做的就儘量不要用回溯法,回溯法實在太容易超時了。。。

​       這道題有個坑就是當n比較大的時候使用int會溢位,導致第一次用動態規劃提交時只通過了5個測點,後來把陣列a的元素型別改為long long就AC了。本題動態規劃的程式碼實現如下:

  #include<iostream>
  using namespace std;
  int main() {
      int n;
      cin>>n;
      long long *a = new long long[n+1]{0,2,3};
      for(int i=3;i<=n;i++) {
          a[i]=a[i-1]+a[i-2];
      }
      cout<<a[n]<<endl;
      return 0;
  }

 

2. Missing number

Given a positive integer n(n≤40), pick n-1 numbers randomly from 1 to n and concatenate them in random order as a string s, which means there is a missing number between 1 and n. Can you find the missing number?(Notice that in some cases the answer will not be unique, and in these cases you only need to find one valid answer.)

Examples:

  Input: 20
         81971112205101569183132414117
  Output: 16

中文題意:給定正整數n(n≤40),從1到n中隨機選擇n-1個數,並將它們以隨機順序連線為字串s,這意味著在1和n之間有一個缺失的數字。你能找到那個缺失的數字嗎?(請注意在某些情況下答案不唯一,此時你只需要找到一個有效的答案。)

解題思路:這題乍一看似乎和LeetCode 268 Missing Number十分相似,但實則大相徑庭,因為LeetCode的Missing Number是把選中的n-1個數字以int陣列的形式直接告訴我們了,而這題n-1個數字是以隨機順序連線在一起形成的字串,沒有告訴我們是如何劃分的,要找出缺失的數字也沒那麼簡單了。

​       注意到題中限制了n≤40,為什麼n的上界取這麼小呢?我覺得這就是在暗示這題解法的時間複雜度比較高,所以自然應該想到回溯法。分析本題特點,只要能找到字串s的合理劃分方法,從s中提取出n-1個數字,缺失的那個數字自然就找出來了,所以本題的關鍵就在於字串s的劃分。具體怎麼劃分呢?不妨這樣考慮,設字串s的長度為n,則對於s中的第i(1≤i≤n-1)個字元,它要麼與第i+1個字元結合成一個數字,要麼自己單獨作為一個數字,對於第n個字元則只能單獨作為一個數字。可能有人要問了,第i個字元不是還可以向前和第i-1個字元結合嗎?當然可以,但是這種情況就和第i-1個字元向後與第i個字元結合的情況重疊了,所以對這種情況不做考慮。如此一來就是n個字元,除第n個字元外每個字元有兩種選擇,這是不是和揹包問題很像呢?所以用回溯法應該是沒問題的。向下一層左子樹搜尋的約束條件就是由第i個字元與第i+1個字元結合得到的數字落在1到n的範圍內且之前未被劃分出來,向下一層右子樹搜尋的約束條件就是由第i個字元單獨形成的數字落在1到n的範圍內且之前未被劃分出來。如若當前字元為'0',說明之前的劃分有誤(因為'0'既不能和後一個字元結合,也不能單獨成數),應該直接返回上一層。如何知道已經找到了正確的劃分呢?只需要在搜尋到葉子結點時檢驗是不是已經劃分出n-1個數字即可。

  這題需要注意的是對於某些測點可能會有兩個答案,比如n=27,s="37258161810262717111420212324131912956152241212345678910" ,s有兩種可能的劃分,一種是“3 7 25 8 16 18 10 26 27 17 11 14 20 2 1 23 24 13 19 12 9 5 6 15 22 4”,另一種是“3 7 25 8 16 18 10 26 27 17 11 14 20 21 23 24 13 19 1 2 9 5 6 15 22 4”,前者得到的答案是21,後者得到的答案是12,兩個答案都是合理答案,至於導致答案不唯一的原因請讀者自行揣摩。(當然想不明白的歡迎在評論區裡提問~)

​       有一點我要坦白:很遺憾考試時我並沒有做出這題,由於當時第一題用回溯法超時了,這題又被LeetCode上的Missing Number帶偏了思路,所以考試時壓根就沒往回溯法去想,只靠一點騙分技巧騙了點分。考完以後才想到這題也可以用回溯法,然後自己試著用程式碼實現了一下。為了便於除錯,本題的測點全部隨機產生,經檢驗在100000個測點的情況下依然是可以AC的,除錯的時候測了下時間,當n接近40的時候回溯部分的耗時也在10^-5~10^-4 s量級,應該不用擔心TLE。回溯法的實現程式碼如下:

 #include<iostream>
  #include<vector>
  #include<set>
  #include<string>
  #include<sstream>
  #include<ctime>
  #include<algorithm>
  using namespace std;
  ​
  const int total=100000; //測點總數
  int n;
  string s;
  void createSample(int& ans,vector<int>& split);
  void backtrack(int index,int cnt,vector<bool>& existed,set<int>& myAns);
  ​
  int main() {
      srand((unsigned int)time(NULL));
      int num=0;  //通過的測點數
      for(int i=0;i<total;i++) {
          int ans;    //缺失的數字,即本題答案
          vector<int> split;  //測點字串產生時的劃分方法
          n=rand()%40+1;
          s.clear();
          createSample(ans,split);
          set<int> myAns;     //使用回溯法得到的答案
          vector<bool> existed(n+1,false);    //標記已劃分出來的數字,existed[i]=true表示數字i已劃分出來
          backtrack(0,0,existed,myAns);
          set<int>::iterator it;
          it=myAns.find(ans);
          if(it!=myAns.end()) {
              num++;
          }
          else {
              cout<<n<<endl<<s<<endl;
              cout<<"Split:";
              for(auto n : split) {
                  cout<<n<<' ';
              }
              cout<<endl;
              cout<<"Your answer:";
              for(set<int>::iterator it=myAns.begin();it!=myAns.end();it++ ) {
                  cout<<*it<<' ';
              }
              cout<<endl;
              cout<<"Expected answer:"<<ans<<endl;
              cout<<"Passed/Total: "<<num<<'/'<<total<<endl;
              cout<<"Wrong answer"<<endl;
              break;
          }
      }
      if(num==total) {
          cout<<"Passed/Total: "<<num<<'/'<<total<<endl;
          cout<<"Accepted"<<endl;
      }
      return 0;
  }
  ​
  //隨機產生測點字串s,缺失的數字存放在ans中,字串的劃分方法存放在split中
  void createSample(int& ans,vector<int>& split) {
      int m,num=0;
      vector<bool> existed(n+1,false);
      while(num < n-1) {  //每一輪隨機產生一個數字m,直至產生n-1個數字為止
          m=rand()%n+1;
          //檢查數字m之前是否已經產生過,只有未產生過的數字才能加入到字串中
          if(existed[m]==false) {
              //將int轉換為string,加入到字串s中,並將數字m儲存到split中
              stringstream ss;
              string tmp;
              ss<<m;
              ss>>tmp;
              s+=tmp;
              existed[m]=true;
              split.push_back(m);
              num++;
          }
      }
      vector<bool>::iterator it;
      it=find(existed.begin()+1,existed.end(),false);
      ans=it-existed.begin();
  }
  ​
  //回溯法求解字串s的劃分,index為字串s的下標,cnt為已經劃分出的數字個數,existed標記已經劃分出了的數字,myAns為求解出的缺失的數字
  void backtrack(int index,int cnt,vector<bool>& existed,set<int>& myAns) {
      if(index >= s.length()) {
          //已經劃分出n-1個數字,existed中未被標記的就是缺失的數字
          if(cnt==n-1) {
              vector<bool>::iterator it;
              it=find(existed.begin()+1,existed.end(),false);
              myAns.insert(it-existed.begin());
          }
          return;
      }
      if(s[index]=='0')   return;  //用於剪枝
      //下標為index的字元與下標為index+1的字元組合成一個數字
      if(index < s.length()-1) {
          int tmp1=(s[index]-'0')*10+s[index+1]-'0';
          if(tmp1 >= 1&&tmp1 <= n&&existed[tmp1]==false) {
              existed[tmp1]=true;
              cnt++;
              backtrack(index+2,cnt,existed,myAns);
              cnt--;
              existed[tmp1]=false;
          }
      }
      //下標為index的字元單獨作為一個數字
      int tmp2=s[index]-'0';
      if(tmp2 >= 1&&tmp2 <= n&&existed[tmp2]==false) {
          existed[tmp2]=true;
          cnt++;
          backtrack(index+1,cnt,existed,myAns);
          cnt--;
          existed[tmp2]=false;
      }
  }

      機試還有一道題,但是題目我記不太清了,考試的時候就粗略看了下題目,覺得沒啥想法,直接放棄了。考完google了一下題目裡的一些關鍵詞,並沒有搜到原題,連相似的題目都沒找到,所以這裡也就不介紹了。

​      總體來說南大的機試題還是不算難的,考察的都是比較基本的演算法,從近幾年的機試題來看,如果是三道題的話,一般第一題是動態規劃,第二題是DFS(回溯法)或者BFS,第三題就不好說了。考試按測點給分,三道題每題十個測點,一個測點10分,滿分300分。然而往年也有四道題選做兩道,按ACM形式排名的,就是提交沒有AC的話要罰時,按AC的題數和總用時排名,那種就比較坑了,不知道以後會不會又採用那種形式。

       發表這篇部落格以前發現網上已經有很多關於2018南京大學夏令營機試的部落格了,題目卻不盡相同,這裡解釋一下:南京大學計算機夏令營是分了本地和外地兩批的,本地的夏令營在七月十幾號就辦完了,外地的夏令營是在7月24號到26號,26號進行機試和麵試。本地的夏令營機試題和外地的夏令營機試題肯定不一樣。由於人數太多(今年報名3000+,入營400+),外地的在考核的時候又分了兩批,一批上午面試下午機試,另一批上午機試下午面試,上午的機試題和下午的機試題當然也不一樣了。

       我是7月26號下午機試的,之所以到現在才寫這篇部落格,是因為南大夏令營結束以後就旅遊去了,旅遊回來以後又忙著折騰自己的VPS、追美劇,加上以前還沒在CSDN上發過部落格,現在才心血來潮突然想寫篇部落格233。 第一次在CSDN上發表部落格,希望能幫助到大家,文章裡有錯誤的話歡迎大家在評論區指出哦。