Advertisement
Fastrail08

Count Binary Strings

May 22nd, 2025
96
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 7.88 KB | None | 0 0
  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. //QUESTION - (PRINT BINARY STRING)
  4. //https://www.youtube.com/watch?v=nqrXHJWMeBc&list=PL-Jc9J83PIiG8fE6rj9F5a6uyQ5WPdqKy&index=18
  5.  
  6. //recursive printing code
  7. void printBinaryString(int pos, string pathString, int &n){
  8.     if(pos >= n){
  9.         cout << pathString << '\n';
  10.         return;
  11.     }
  12.     if(pathString.empty() || pathString[pos - 1] == '1'){
  13.       printBinaryString(pos + 1, pathString + '0', n);  
  14.     }
  15.     printBinaryString(pos + 1, pathString + '1', n);
  16. }
  17.  
  18. //memo code
  19.  
  20. // NOTE - Approach-
  21. /*
  22. To perform memoisation, we need to watch out for the params that are changing in the recursive code.
  23.  
  24. Here for eg, Initially it would feel like the pos variable & pathString are changing.
  25.  
  26. IMPORTANT - pos represents the bit for which the options are being explored (either 0 or 1)
  27. The pathString is changing and will be unique for each branch as we explored unique options to create a new branch.
  28. So we cannot memoise by memo(i, pathString) = given the ith position bit(pos) and pathString formed so far, how many valid binary strings.
  29. As this will not have any overlapping subproblems, because pathString for every state will be unique.(draw tree, easy)
  30.  
  31. But analyse the code again:
  32. 1) pos is changing, and hence pos will be used as an identifier = memo(pos, ?)
  33. But pos alone is not enough to identify the overlapping subproblems here.
  34. for eg - pos = 2, 2 length binary string, we could have -
  35.  
  36. 00, 01, 10, 11
  37.  
  38. out of which 00 is invalid, so we are left with 01, 10, 11. These are all valid binary strings of length 2.
  39. Let's see for each, how many unique valid binary strings they are responsible for. The number of binary strings that they can form is equal to number of branches they can form. As each branch was explored only once and with unique option at that level, each branch will generate a unique string in the base case.
  40.  
  41. memo(10) = 1 = this string has only one option to explore to, which is option '1', as the last char is 0, it cannot explore option '0'. It will explore create only 1 branch at level memo(10), and hence responsible for a single valid binary string
  42. memo(01) = 2 = this string has both options {0, 1}
  43. memo(11) = 2 = this string has both options {0, 1} as they both end with 1, and they could explore both options from their level {0, 1}. Both of these will explore both options, creating 2 branches that will futher lead to atleast 2 valid unique binary strings.
  44.  
  45. so if we only take memo(pos) and define it as "every binary string of length 'pos' will generate the same amount of valid binary strings upto n" & these are overlapping subproblems, we can easily see that this is not the case, as count(01) = count(11) != count(10)
  46.  
  47. 2) Ask yourself while looking at code below - Are we actually using pathString to make the decision?
  48. Why did we use pathString in the first place?
  49. Ans 2) As we are only generating valid cases, so if we are making the decision/exploring options for the current position 'pos', to avoid consecutive 0s, we need to make sure that we are exploring the option 0 for level 'pos' only if level 'pos - 1' has a 1, otherwise it will result in consecutive 0s.
  50.  
  51. We maintained the pathString to check what the last character was, and on the basis of that we choose to explore option '0' for any level 'pos'.
  52.  
  53. We are only checking the last character that if it is 0 or 1.
  54.  
  55. So our memo identifiers would be = memo(pos, bit).
  56. And the possible states would be a mapping of this
  57. pos = {0, 1, 2, 3, ---- n - 1}
  58. bit = {0, 1}
  59.  
  60. Define it as "every binary string of length 'pos' and last char 'bit' will generate the same amount of valid binary strings upto n" and the strings falling into a memo category will be classified as overlapping subproblems.
  61.  
  62. so for above example pos = 2, valid binary string = 01, 10, 11
  63.  
  64. memo(2, 0) = 10, string of length 2 ending with 0
  65. memo(2, 1) = 01, 11 , as both strings of length 2 ending with 1
  66.  
  67. These are correctly overlapping as both 01, 11, will generate valid binary strings if continued up to n as they provide the same branching factor of 2 at their respective level, and hence they are correctly classified as the same problem getting solved twice by state memo(2, 1).
  68.  
  69. So all in all, it depends on the position of bit in consideration, and if it ends with 0 or 1, which gives us information about how many branches it will created if the tree is extended up to the level n. So for n = 8, i = 3 and strings of length 1 ending with 1, which are {011, 101, 111} will be creating exactly same NUMBER/AMOUNT of branches, though unique binary strings. The number of branches will remain the same as branching factor will be equal for these strings.
  70.  
  71.  
  72. */
  73. int countBinaryString(int pos, string pathString, int &n, vector<vector<int> > &memo){
  74.     if(pos >= n){
  75.         return 1;
  76.     }
  77.     //memo[pos][last char in pathString]
  78.     if(!pathString.empty() && memo[pos][pathString[pos - 1] - '0'] != 0){
  79.         return memo[pos][pathString[pos - 1] - '0'];
  80.     }
  81.     int count = 0;
  82.     if(pathString.empty() || pathString[pos - 1] == '1'){
  83.         count += countBinaryString(pos + 1, pathString + '0', n, memo);
  84.     }
  85.     count += countBinaryString(pos + 1, pathString + '1', n, memo);
  86.     return memo[pos][pathString[pos - 1] - '0'] = count;
  87. }
  88.  
  89. // only last char passed, instead of whole binary string, because at every step we only check for the last char anyways
  90. int countBinaryStringRevised(int pos, int lastChar, int &n, vector<vector<int> > &memo){
  91.     if(pos >= n){
  92.         //we explored options for every position (so pos = n) at base case, and we always a valid binary string in base case, as we made only valid calls. If there were invalid calls while doing recursion, i.e. if we explored option '0' call from a level where last char was 0, we would have generated an invalid case, where strings of nature '00' might be there, and then we would have to filter it and return 1 for only the valid case.
  93.         return 1;
  94.     }
  95.     //memo check
  96.     if(lastChar != -1 && memo[pos][lastChar] != 0){
  97.         return memo[pos][lastChar];
  98.     }
  99.    
  100.     int count = 0;
  101.     //option 0 for level pos
  102.     if(pos == 0 || lastChar == 1){
  103.         // only called if exploring option for 1st position OR last char was 1 at previous level/position (to avoid consecutive 0s)
  104.         count += countBinaryStringRevised(pos + 1, 0, n, memo);
  105.     }
  106.    
  107.     //option 1 for level pos
  108.     count += countBinaryStringRevised(pos + 1, 1, n, memo);
  109.     if(lastChar != -1){
  110.         memo[pos][lastChar] = count;
  111.     }
  112.     return count;
  113. }
  114.  
  115. int countBinaryStringDP(int &n){
  116.     /*Storage and Meaning - dp[i][0] = about ith position bit out of n - 1 & string ending with 0
  117.                             dp[i][1] = about ith position bit out of n - 1 & string ending with 1
  118.                            
  119.                             dp[i][0] = count of valid binary strings of length (i) ending with 0
  120.                             dp[i][1] = count of valid binary strings of length (i) ending with 1
  121.       */
  122.     vector<vector<int> > dp(n, vector<int>(2, 0));
  123.    
  124.     /*
  125.     Direction - smallest problem is at dp[0][0] and dp[0][1].
  126.     */
  127.     dp[0][0] = 1;
  128.     dp[0][1] = 1;
  129.    
  130.     // Travel & Solve
  131.    
  132.     for(int i = 1; i < n; i++){
  133.         dp[i][0] = dp[i - 1][1];
  134.         dp[i][1] = dp[i - 1][0] + dp[i - 1][1];
  135.     }
  136.     //Both these should be returned as these 2 cells mean
  137.     // dp[n - 1][0] = binary strings of length n, that ends with 0
  138.     // dp[n - 1][1] = binary strings of length n, that ends with 1
  139.     // but both are valid binary strings of length n that have no consecutive 0s
  140.     return dp[n - 1][0] + dp[n - 1][1];
  141.    
  142. }
  143.  
  144. int main() {
  145.     // your code goes here
  146.     int n;
  147.     cin >> n;
  148.     // printBinaryString(0, "", n);
  149.     //vector<vector<int> > memo(n, vector<int> (2, 0));
  150.     //cout << countBinaryStringRevised(0, -1, n, memo) << '\n';
  151.     cout << countBinaryStringDP(n) << '\n';
  152. }
  153.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement