LeetCode 926 - Flip String to Monotone Increasing


https://leetcode.com/problems/flip-string-to-monotone-increasing/
A string of '0's and '1's is monotone increasing if it consists of some number of '0's (possibly 0), followed by some number of '1's (also possibly 0.)
We are given a string S of '0's and '1's, and we may flip any '0' to a '1' or a '1' to a '0'.
Return the minimum number of flips to make S monotone increasing.
https://zxi.mytechroad.com/blog/dynamic-programming/leetcode-926-flip-string-to-monotone-increasing/
l[i] := number of flips to make S[0] ~ S[i] become all 0s.
r[i] := number of flips to make S[i] ~ S[n – 1] become all 1s.
Try all possible break point, S[0] ~ S[i – 1] are all 0s and S[i] ~ S[n-1] are all 1s.
ans = min{l[i – 1] + r[i]}
  int minFlipsMonoIncr(string S) {
    const int n = S.size();
    vector<int> l(n + 1); // 1 -> 0
    vector<int> r(n + 1); // 0 -> 1
    l[0] = S[0] - '0';
    r[n - 1] = '1' - S[n - 1];
    for (int i = 1; i < n; ++i)
      l[i] = l[i - 1] + S[i] - '0';
    for (int i = n - 2; i >= 0; --i)
      r[i] = r[i + 1] + '1' - S[i];
    int ans = r[0]; // all 1s.
    for (int i = 1; i <= n; ++i)
      ans = min(ans, l[i - 1] + r[i]);
    return ans;
  }

X. 
这个题和买卖股票有点类似,都需要使用一个二维dp数组,分别保存的是以0结尾或者以1结尾的字符串需要翻转的最小次数。

为了方便,给dp数组多了一个空间,表示最前面的字符串还没有开始的时候,肯定不需要做任何翻转。

那么,当我们遍历到字符串的i位置时:

如果这个位置的字符是'0',那么:
当前以0结尾的dp数组等于前面的以0结尾的dp,即不需要做任何操作,此时前面必须是0结尾;
当前以1结尾的dp数组等于Min(前面的以0结尾的dp + 1,前面的以1结尾的dp + 1)。这里的含义是一种情况为前面的0到前面的那个位置结束,把当前的0翻转成1;另一种情况是前面一位以1结束不变,把当前的0翻转成1。需要求这两个情况的最小值。此时前面可以以0或者1结尾。
如果这个位置的字符是'1',那么:
当前以0结尾的dp数组等于前面的以0结尾的dp + 1,即把当前的1翻转成0,此时前面只能以0结尾;
当前以1结尾的dp数组等于Min(前面的以0结尾的dp,前面的以1结尾的dp)。这里的含义是该位置以1结尾需要翻转多少次呢?当然是前面翻转0或者1的次数最小值,因为当前的1一定不用翻转,而前面无论怎么翻转都能满足条件。此时前面可以以0或者1结尾。
总结一下思路,首先一定要明白dp数组是代表以这个第二维度数字结尾的状态数,比如dp[i][0]就是第i个数字以0结尾的情况下,需要翻转的个数。然后,要明白的是如果遍历到的这个字符并没有限制死我们是否要翻转它,所以翻转不翻转都要考虑到,即这个对应位置变成1或者0两种情况下dp怎么更新。更新的方式是看前面一个状态,从前一个状态转成现在要变成的状态,需要做哪些操作,翻转次数怎么变化。


  int minFlipsMonoIncr(string S) {
    const int n = S.length();
    vector<vector<int>> dp(n + 1, vector<int>(2));
    for (int i = 1; i <= n; ++i) {
      if (S[i - 1] == '0') {
        dp[i][0] = dp[i - 1][0];
        dp[i][1] = min(dp[i - 1][0], dp[i - 1][1]) + 1;
      } else {
        dp[i][0] = dp[i - 1][0] + 1;
        dp[i][1] = min(dp[i - 1][0], dp[i - 1][1]);
      }
    }
    return min(dp[n][0], dp[n][1]);
  }


  int minFlipsMonoIncr(string S) {
    const int n = S.length();
    int dp0 = 0;
    int dp1 = 0;
    for (int i = 1; i <= n; ++i) {
      int tmp0 = dp0 + (S[i - 1] == '1');
      dp1 = min(dp0, dp1) + (S[i - 1] == '0');
      dp0 = tmp0;
    }
    return min(dp0, dp1);
  }



https://blog.csdn.net/fuxuemingzhu/article/details/83247054
常规的做法是,我们使用Prefix数组保存每个位置之前的1有多少个。因为我们最终的目标是变成前面是0后面是1的字符串,所以,可以通过过一遍数组,要把遍历到的位置前面都变0后面都变1,需要计算每个位置前面有多少个1加上后面有多少个0。因为前面的1要翻转成0,后面的0要翻转成1.

总之,只需要先求出每个位置前面的1的个数是多少,那么,再次遍历,求每个位置前面的1个数和后面0个数的和的最小值即可。

使用的是P数组保存每个位置前面1的个数。那么后面0的个数是:总的0的个数(即,总个数减去总的1的个数) - 前面0的个数(即,现在的位置索引减去前面1的个数)。

Approach 1: Prefix Sums
For say a 5 digit string, the answer is either '00000''00001''00011''00111''01111', or '11111'. Let's try to calculate the cost of switching to that answer. The answer has two halves, a left (zero) half, and a right (one) half.
Evidently, it comes down to a question of knowing, for each candidate half: how many ones are in the left half, and how many zeros are in the right half.
We can use prefix sums. Say P[i+1] = A[0] + A[1] + ... + A[i], where A[i] = 1 if S[i] == '1', else A[i] = 0. We can calculate P in linear time.
Then if we want x zeros followed by N-x ones, there are P[x] ones in the start that must be flipped, plus (N-x) - (P[N] - P[x]) zeros that must be flipped. The last calculation comes from the fact that there are P[N] - P[x] ones in the later segment of length N-x, but we want the number of zeros.
Algorithm
For example, with S = "010110": we have P = [0, 0, 1, 1, 2, 3, 3]. Now say we want to evaluate having x=3 zeros.
There are P[3] = 1 ones in the first 3 characters, and P[6] - P[3] = 2 ones in the later N-x = 3characters.
So, there is (N-x) - (P[N] - P[x]) = 1 zero in the later N-x characters.
We take the minimum among all candidate answers to arrive at the final answer.

  public int minFlipsMonoIncr(String S) {
    int N = S.length();
    int[] P = new int[N + 1];
    for (int i = 0; i < N; ++i)
      P[i + 1] = P[i] + (S.charAt(i) == '1' ? 1 : 0);

    int ans = Integer.MAX_VALUE;
    for (int j = 0; j <= N; ++j) {
      ans = Math.min(ans, P[j] + N - j - (P[N] - P[j]));
    }

    return ans;
  }

X.
https://www.cnblogs.com/seyjs/p/9828593.html
转换后的字符串有两种形式,一个是全为0,另一个是前半部分全是0后半部分全是1。第一种情况的翻转次数是S中1的个数;第二种的情况,我们只要找出转换后的字符串第一个1所在的位置即可。设第一个所在的位置是i,那么S[0:i-1]区间内所有1都要变成0,而S[i:-1]区间内所有的0要变成1,总的翻转次数就是前一段区间内1的个数加上后一段区间内0的个数即可。遍历S,即可求出第二种情况的最小值,再与第一种情况求得的值比较取最小值即可。
    def minFlipsMonoIncr(self, S):
        """
        :type S: str
        :rtype: int
        """
        t_count_1 = S.count('1')
        t_count_0 = len(S) - t_count_1
        res = t_count_1

        count_0 = 0
        count_1 = 0
        for i,v in enumerate(S):
            if v == '0':
                count_0 += 1
            else:
                #count_1 += 1
                # if convert this 1 to 0
                res = min(res,count_1 + (t_count_0 - count_0))
                count_1 += 1
        return res
https://leetcode.com/problems/flip-string-to-monotone-increasing/discuss/183896/Prefix-Suffix-Java-O(N)-One-Pass-Solution-Space-O(1)
  1. Skip 0's until we encounter the first 1.
  2. Keep track of number of 1's in onesCount (Prefix).
  3. Any 0 that comes after we encounter 1 can be a potential candidate for flip. Keep track of it in flipCount.
  4. If flipCount exceeds oneCount - (Prefix 1's flipped to 0's)
    a. Then we are trying to flip more 0's (suffix) than number of 1's (prefix) we have.
    b. Its better to flip the 1's instead.
public int minFlipsMonoIncr(String S) {
 if(S == null || S.length() <= 0 )
  return 0;

 char[] sChars = S.toCharArray();
 int flipCount = 0;
 int onesCount = 0;

 for(int i=0; i<sChars.length; i++){
  if(sChars[i] == '0'){
   if(onesCount == 0) continue;
   else flipCount++;
  }else{
   onesCount++;
  }
  if(flipCount > onesCount){
   flipCount = onesCount;
  }
 }
 return flipCount;
}
https://leetcode.com/problems/flip-string-to-monotone-increasing/discuss/189751/C%2B%2B-one-pass-DP-solution-0ms-O(n)-or-O(1)-one-line-with-explaination.
This is a typical case of DP.
Let's see the sub-question of DP first.
Suppose that you have a string s, and the solution to the mono increase question is already solved. That is, for string scounter_flip flips are required for the string, and there were counter_one '1's in the original string s.
Let's see the next step of DP.
Within the string s, a new incoming character, say ch, is appended to the original string. The question is that, how should counter_flip be updated, based on the sub-question? We should discuss it case by case.
  • When '1' comes, no more flip should be applied, since '1' is appended to the tail of the original string.
  • When '0' comes, things become a little bit complicated. There are two options for us: flip the newly appended '0' to '1', after counter_flip flips for the original string; or flip counter_one '1' in the original string to '0'. Hence, the result of the next step of DP, in the '0' case, is std::min(counter_flip + 1, counter_one);.
    int minFlipsMonoIncr(const std::string& S, int counter_one  = 0, int counter_flip = 0) {
        for (auto ch : S) {
            if (ch == '1') {
                ++counter_one;
            } else {
                ++counter_flip;
            }
            counter_flip = std::min(counter_one, counter_flip);
        }
        return counter_flip;
    }
X. https://leetcode.com/problems/flip-string-to-monotone-increasing/discuss/183851/C%2B%2BJava-4-lines-O(n)-or-O(1)-DP
We need to split the array into left '0' and right '1' sub-arrays, so that sum of '1' -> '0' flips (left) and '0' -> '1' flips (right) is minimal.
  • Count of '0' -> '1' flips going left to right, and store it in f0.
  • Count of '1' -> '0' flips going right to left, and store it in f1.
  • Find a the smallest f0[i] + f1[i].
int minFlipsMonoIncr(string S) {
    vector<int> f0(S.size() + 1), f1(S.size() + 1);
    int res = INT_MAX;
    for (auto i = 0; i < S.size(); ++i) {
        f0[i + 1] += f0[i] + (S[i] == '0' ? 0 : 1);
        f1[S.size() - i - 1] += f1[S.size() - i] + (S[S.size() - i - 1] == '0' ? 1 : 0);
    }
    for (auto i = 0; i <= S.size(); ++i) res = min(res, f0[i] + f1[i]);
    return res;
}
Actually, this problem look similar to 122. Best Time to Buy and Sell Stock III. Now with this intuition, can we do better and save some memory?
We can go left to right, and virtually 'move' the split point every time we see that we need less '0' -> '1' than '1' -> '0' flips (f1 = min(f0, f1 + 1 - (c - '0'))).
int minFlipsMonoIncr(string S, int f0 = 0, int f1 = 0) {
    for (auto c : S) {
        f0 += c - '0';
        f1 = min(f0, f1 + 1 - (c - '0'));
    }
    return f1;
}
https://leetcode.com/problems/flip-string-to-monotone-increasing/discuss/183862/Count-prefix-ones-and-suffix-zeros
leftOne[i] represents count of 1s in S[0...i]
rightZero[i] represents count of 0s in S[i...n-1]
Then min = min(leftOne[i] + rightZero[i + 1]), where i = 0, ... n-1
    public int minFlipsMonoIncr(String S) {
        int n = S.length();
        int[] leftOne = new int[n];
        int[] rightZero = new int[n];
        if (S.charAt(0) == '1') {
            leftOne[0] = 1;
        }
        if (S.charAt(n - 1) == '0') {
            rightZero[n - 1] = 1;
        }
        for (int i = 1; i < n; i++) {
            leftOne[i] = S.charAt(i) == '1' ? leftOne[i - 1] + 1 : leftOne[i - 1];
            rightZero[n - 1 - i] = S.charAt(n - 1 - i) == '0' ? rightZero[n - i] + 1 : rightZero[n - i];
        }
        int min = n;
        for (int i = -1; i < n; i++) {
            int left = i == -1 ? 0 : leftOne[i];
            int right = i == n - 1 ? 0 : rightZero[i + 1];
            min = Math.min(min, left + right);
        }
        return min;
    }

X. https://leetcode.com/problems/flip-string-to-monotone-increasing/discuss/183855/Java-DP-solution-O(n)-time-O(1)-space
/* tranverse the string
 * use two variables to record the minimim number of flip need to make the substring from (0 - i) to be MonoIncr with end with 1 or 0;
 * (1) for position i + 1, if preceding string is end with 1, the current string can only end with one, 
 *   so cntEndWithOne can only be used to update the next cntEndWithOne
 * (2) if the preceding string is end with zero, current string can either end with zero or one
 *   so cntEndWithZero can be used to update for both next cntEndWithOne and cntEndWithZero;
 */
    public int minFlipsMonoIncr(String S) {
        if (S == null || S.length() <= 1) return 0;
        int n = S.length();
        int cntEndWithOne = 0, cntEndWithZero = 0;
        
        for (int i = 0; i < n; i++) {
            char c = S.charAt(i);
            cntEndWithOne = Math.min(cntEndWithZero, cntEndWithOne) + (c == '1' ? 0 : 1);
            cntEndWithZero += (c == '0' ? 0 : 1);
        }
        return Math.min(cntEndWithOne, cntEndWithZero);
    }

X. https://zhanghuimeng.github.io/post/leetcode-926-flip-string-to-monotone-increasing/
一个立即可以想到的思路是枚举。枚举每一位作为1的起始点,然后根据这一位左边的1的个数和右边的0的个数算出总的翻转次数,然后取最小值。前缀中1的个数和后缀中0的个数都很好维护。
另一个相当神奇的思路[1]应用了贪心法。在从左向右扫描的过程中,我们动态维护这个分割点,分割点左侧1的数量,分割点右侧1的数量和0的数量;一旦发现分割点右侧0的数量超过了1的数量,说明此时把分割点直接右移到当前扫描点可以减小翻转次数。举个例子:
S = "00011000"
ivirtual split pointleft 1 cntright 0 cntright 1 cnt
00 (| 00011000)000
11 (0| 0011000)000
22 (00| 011000)000
33 (000| 11000)000
43 (000|1 1000)001
53 (000|11 000)002
63 (000|110 00)012
73 (000|1100 0)022
88 (00011000|)200
这个算法的正确性怎么证明……好吧,懒得去想了。大概应该用反证法。

    int minFlipsMonoIncr(string S) {
        int leftOnes = 0, rightOnes = 0, rightZeros = 0;
        for (int i = 0; i < S.size(); i++) {
            if (S[i] == '0') rightZeros++;
            else rightOnes++;
            if (rightZeros > rightOnes) {  // move split point to i
                leftOnes += rightOnes;
                rightOnes = rightZeros = 0;
            }
        }
        return leftOnes + rightZeros;
    }
https://leetcode.com/problems/flip-string-to-monotone-increasing/discuss/183851/C++-4-lines-O%28n%29-or-O%281%29-DP
We need to split the array into left '0' and right '1' sub-arrays, so that sum of '1' -> '0' flips (left) and '0' -> '1' flips (right) is minimal.
  • Count of '0' -> '1' flips going left to right, and store it in f0.
  • Count of '1' -> '0' flips going right to left, and store it in f1.
  • Find a the smallest f0[i] + f1[i].
int minFlipsMonoIncr(string S, int res = INT_MAX) {
    vector<int> f0(S.size() + 1), f1(S.size() + 1);
    for (int i = 1, j = S.size() - 1; j >= 0; ++i, --j) {
        f0[i] += f0[i - 1] + (S[i - 1] == '0' ? 0 : 1);
        f1[j] += f1[j + 1] + (S[j] == '1' ? 0 : 1);
    }
    for (int i = 0; i <= S.size(); ++i) res = min(res, f0[i] + f1[i]);
    return res;
}
image
This reminds me of 122. Best Time to Buy and Sell Stock III. That problem has a DP solution with O(1) memory complexity, so we can try to apply it here. We can go left to right, and virtually 'move' the split point every time we see that we need less '0' -> '1' than '1' -> '0' flips (f1 = min(f0, f1 + 1 - (c - '0'))).
int minFlipsMonoIncr(string S, int f0 = 0, int f1 = 0) {
    for (auto c : S) {
        f0 += c - '0';
        f1 = min(f0, f1 + 1 - (c - '0'));
    }
    return f1;
}
Here is Java version. Note that I am using charAt(i) instead of toCharArray() to keep the memory complexity a constant.
public int minFlipsMonoIncr(String S) {
  int f0 = 0, f1 = 0;
  for (int i = 0; i < S.length(); ++i) {
    f0 += S.charAt(i) - '0';
    f1 = Math.min(f0, f1 + 1 - (S.charAt(i) - '0'));
  }
  return f1;
}

Labels

LeetCode (1432) GeeksforGeeks (1122) LeetCode - Review (1067) Review (882) Algorithm (668) to-do (609) Classic Algorithm (270) Google Interview (237) Classic Interview (222) Dynamic Programming (220) DP (186) Bit Algorithms (145) POJ (141) Math (137) Tree (132) LeetCode - Phone (129) EPI (122) Cracking Coding Interview (119) DFS (115) Difficult Algorithm (115) Lintcode (115) Different Solutions (110) Smart Algorithm (104) Binary Search (96) BFS (91) HackerRank (90) Binary Tree (86) Hard (79) Two Pointers (78) Stack (76) Company-Facebook (75) BST (72) Graph Algorithm (72) Time Complexity (69) Greedy Algorithm (68) Interval (63) Company - Google (62) Geometry Algorithm (61) Interview Corner (61) LeetCode - Extended (61) Union-Find (60) Trie (58) Advanced Data Structure (56) List (56) Priority Queue (53) Codility (52) ComProGuide (50) LeetCode Hard (50) Matrix (50) Bisection (48) Segment Tree (48) Sliding Window (48) USACO (46) Space Optimization (45) Company-Airbnb (41) Greedy (41) Mathematical Algorithm (41) Tree - Post-Order (41) ACM-ICPC (40) Algorithm Interview (40) Data Structure Design (40) Graph (40) Backtracking (39) Data Structure (39) Jobdu (39) Random (39) Codeforces (38) Knapsack (38) LeetCode - DP (38) Recursive Algorithm (38) String Algorithm (38) TopCoder (38) Sort (37) Introduction to Algorithms (36) Pre-Sort (36) Beauty of Programming (35) Must Known (34) Binary Search Tree (33) Follow Up (33) prismoskills (33) Palindrome (32) Permutation (31) Array (30) Google Code Jam (30) HDU (30) Array O(N) (29) Logic Thinking (29) Monotonic Stack (29) Puzzles (29) Code - Detail (27) Company-Zenefits (27) Microsoft 100 - July (27) Queue (27) Binary Indexed Trees (26) TreeMap (26) to-do-must (26) 1point3acres (25) GeeksQuiz (25) Merge Sort (25) Reverse Thinking (25) hihocoder (25) Company - LinkedIn (24) Hash (24) High Frequency (24) Summary (24) Divide and Conquer (23) Proof (23) Game Theory (22) Topological Sort (22) Lintcode - Review (21) Tree - Modification (21) Algorithm Game (20) CareerCup (20) Company - Twitter (20) DFS + Review (20) DP - Relation (20) Brain Teaser (19) DP - Tree (19) Left and Right Array (19) O(N) (19) Sweep Line (19) UVA (19) DP - Bit Masking (18) LeetCode - Thinking (18) KMP (17) LeetCode - TODO (17) Probabilities (17) Simulation (17) String Search (17) Codercareer (16) Company-Uber (16) Iterator (16) Number (16) O(1) Space (16) Shortest Path (16) itint5 (16) DFS+Cache (15) Dijkstra (15) Euclidean GCD (15) Heap (15) LeetCode - Hard (15) Majority (15) Number Theory (15) Rolling Hash (15) Tree Traversal (15) Brute Force (14) Bucket Sort (14) DP - Knapsack (14) DP - Probability (14) Difficult (14) Fast Power Algorithm (14) Pattern (14) Prefix Sum (14) TreeSet (14) Algorithm Videos (13) Amazon Interview (13) Basic Algorithm (13) Codechef (13) Combination (13) Computational Geometry (13) DP - Digit (13) LCA (13) LeetCode - DFS (13) Linked List (13) Long Increasing Sequence(LIS) (13) Math-Divisible (13) Reservoir Sampling (13) mitbbs (13) Algorithm - How To (12) Company - Microsoft (12) DP - Interval (12) DP - Multiple Relation (12) DP - Relation Optimization (12) LeetCode - Classic (12) Level Order Traversal (12) Prime (12) Pruning (12) Reconstruct Tree (12) Thinking (12) X Sum (12) AOJ (11) Bit Mask (11) Company-Snapchat (11) DP - Space Optimization (11) Dequeue (11) Graph DFS (11) MinMax (11) Miscs (11) Princeton (11) Quick Sort (11) Stack - Tree (11) 尺取法 (11) 挑战程序设计竞赛 (11) Coin Change (10) DFS+Backtracking (10) Facebook Hacker Cup (10) Fast Slow Pointers (10) HackerRank Easy (10) Interval Tree (10) Limited Range (10) Matrix - Traverse (10) Monotone Queue (10) SPOJ (10) Starting Point (10) States (10) Stock (10) Theory (10) Tutorialhorizon (10) Kadane - Extended (9) Mathblog (9) Max-Min Flow (9) Maze (9) Median (9) O(32N) (9) Quick Select (9) Stack Overflow (9) System Design (9) Tree - Conversion (9) Use XOR (9) Book Notes (8) Company-Amazon (8) DFS+BFS (8) DP - States (8) Expression (8) Longest Common Subsequence(LCS) (8) One Pass (8) Quadtrees (8) Traversal Once (8) Trie - Suffix (8) 穷竭搜索 (8) Algorithm Problem List (7) All Sub (7) Catalan Number (7) Cycle (7) DP - Cases (7) Facebook Interview (7) Fibonacci Numbers (7) Flood fill (7) Game Nim (7) Graph BFS (7) HackerRank Difficult (7) Hackerearth (7) Inversion (7) Kadane’s Algorithm (7) Manacher (7) Morris Traversal (7) Multiple Data Structures (7) Normalized Key (7) O(XN) (7) Radix Sort (7) Recursion (7) Sampling (7) Suffix Array (7) Tech-Queries (7) Tree - Serialization (7) Tree DP (7) Trie - Bit (7) 蓝桥杯 (7) Algorithm - Brain Teaser (6) BFS - Priority Queue (6) BFS - Unusual (6) Classic Data Structure Impl (6) DP - 2D (6) DP - Monotone Queue (6) DP - Unusual (6) DP-Space Optimization (6) Dutch Flag (6) How To (6) Interviewstreet (6) Knapsack - MultiplePack (6) Local MinMax (6) MST (6) Minimum Spanning Tree (6) Number - Reach (6) Parentheses (6) Pre-Sum (6) Probability (6) Programming Pearls (6) Rabin-Karp (6) Reverse (6) Scan from right (6) Schedule (6) Stream (6) Subset Sum (6) TSP (6) Xpost (6) n00tc0d3r (6) reddit (6) AI (5) Abbreviation (5) Anagram (5) Art Of Programming-July (5) Assumption (5) Bellman Ford (5) Big Data (5) Code - Solid (5) Code Kata (5) Codility-lessons (5) Coding (5) Company - WMware (5) Convex Hull (5) Crazyforcode (5) DFS - Multiple (5) DFS+DP (5) DP - Multi-Dimension (5) DP-Multiple Relation (5) Eulerian Cycle (5) Graph - Unusual (5) Graph Cycle (5) Hash Strategy (5) Immutability (5) Java (5) LogN (5) Manhattan Distance (5) Matrix Chain Multiplication (5) N Queens (5) Pre-Sort: Index (5) Quick Partition (5) Quora (5) Randomized Algorithms (5) Resources (5) Robot (5) SPFA(Shortest Path Faster Algorithm) (5) Shuffle (5) Sieve of Eratosthenes (5) Strongly Connected Components (5) Subarray Sum (5) Sudoku (5) Suffix Tree (5) Swap (5) Threaded (5) Tree - Creation (5) Warshall Floyd (5) Word Search (5) jiuzhang (5)

Popular Posts