Showing posts with label DP. Show all posts
Showing posts with label DP. Show all posts

Saturday, August 2, 2014

[LeetCode] Maximum Subarray

Problem Statement (link):
Find the contiguous subarray within an array (containing at least one number) which has the largest sum.
For example, given the array [−2,1,−3,4,−1,2,1,−5,4], the contiguous subarray [4,−1,2,1] has the largest sum = 6.
Analysis:
Suppose we scan from left to right, each element could either be counted in the max-sum subarray, or not. If we use an array to store this information, we probably could solve the problem in O(n) time.

Thus, the idea is to use DP to solve the problem. Specifically, the DP array stores the maximum sum we have so far if we count the current element in. In addition, we use an integer to store the maximum sum we have so far.

Code:
class Solution {
public:
    int maxSubArray(int A[], int n) {
        if (n==0) return 0;
        int maxSum=INT_MIN; // max sum so far
        vector<int> dp(n+1);  // dp[i+1]: the maxSum of subarray which ends with A[i];
        dp[0]=0;
        for (int i=0; i<n; i++) {
            if (dp[i]<0) dp[i+1]=A[i];
            else dp[i+1]=dp[i]+A[i];
            maxSum=max(maxSum, dp[i+1]);
        }
        return maxSum;
    }
};


Friday, July 25, 2014

[LeetCode] Interleaving String

Problem Statement (link):
Given s1s2s3, find whether s3 is formed by the interleaving of s1 and s2.
For example,
Given:
s1 = "aabcc",
s2 = "dbbca",
When s3 = "aadbbcbcac", return true.
When s3 = "aadbbbaccc", return false.
Analysis:
It reminds me of the Edit Distance problem. As we could break this problem down to some smaller problems, i.e., consider if s1[:i-1] and s2[:j-1] could build s[:i+j-1], we could use DP.

We could construct a matrix dp[s1.length()+1][s2.length()+1], remember in DP we usually leave one more extra space for initial condition, which is both s1 and s2 are blank string in this problem. Each entry dp[i][j] indicates whether s1[i-1] and s2[j-1] could build  s[:i+j-1].

Now consider the transfer function. dp[i][j] is true if either of the following cases is true:
1) current char in s1 is same as current char in s3, and previous dp entry in the same row is true
i.e., s1[i-1]==s3[i+j-1] && dp[i-1][j]) == true
2) current char in s2 is same as current char in s3, and previous dp entry in the same col is true
i.e., s2[j-1]==s3[i+j-1] && dp[i][j-1] == true

Our final answer is in the last entry of the DP matrix.

The time complexity of the algorithm is O(len1 * len2), where the two lengths are the lengths of s1 and s2, respectively.

Code:
class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int len1=s1.length(), len2=s2.length();
        if (len1+len2!=s3.length()) return false;
        vector<vector<bool>> dp(len1+1, vector<bool> (len2+1, false));

        // initial
        dp[0][0]=true;
        for (int i=1; i<=len1; i++)
            if (s1[i-1]==s3[i-1] && dp[i-1][0]) dp[i][0]=true;
        for (int j=1; j<=len2; j++)
            if (s2[j-1]==s3[j-1] && dp[0][j-1]) dp[0][j]=true;

        // update dp
        for (int i=1; i<=len1; i++) {
            for (int j=1; j<=len2; j++) {
                dp[i][j]=(s1[i-1]==s3[i+j-1] && dp[i-1][j]) || (s2[j-1]==s3[i+j-1] && dp[i][j-1]);
            }
        }
        return dp[len1][len2];
    }
};



Tuesday, June 10, 2014

[LeetCode] Unique Paths I && II

Unique Paths I

Problem Statement (link):
A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).
The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).
How many possible unique paths are there?

Above is a 3 x 7 grid. How many possible unique paths are there?
Note: m and n will be at most 100.
Analysis:
First, notice that this is a DP problem, the number of paths to reach (i, j) equals to the number of paths to reach (i-1, j) + the number of paths to reach (i, j-1), except the first row and column, where the number of paths are all 1.

It's obvious that we could use a 2D DP to solve it. However, if we consider reusing a 1D DP vector, we could solve it with O(n) time complexity. The time complexity is O(m*n).

Code:
Sol 1: 2D DP
class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> res(m, vector<int>(n, 0));
        for (int i=0; i<m; i++)
            res[i][0]=1;
        for (int j=0; j<n; j++)
            res[0][j]=1;

        for (int i=1; i<m; i++)

            for (int j=1; j<n; j++)
                res[i][j]=res[i-1][j]+res[i][j-1];
        return res[m-1][n-1];
    }
};

Sol 2: 1D DP
class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<int> res(n, 1);
        for (int i=1; i<m; i++)
            for (int j=1; j<n; j++)
                res[j]=res[j-1]+res[j];
        return res[n-1];
    }
};


Unique Paths II

Problem Statement (link):
Follow up for "Unique Paths":
Now consider if some obstacles are added to the grids. How many unique paths would there be?
An obstacle and empty space is marked as 1 and 0 respectively in the grid.
For example,
There is one obstacle in the middle of a 3x3 grid as illustrated below.
[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]
The total number of unique paths is 2.
Note: m and n will be at most 100.
Analysis:
The basic idea is same as the previous problem. Just a few special cases that we need to take care of when encounter obstacles.

- For the first row and first column, once we encounter an obstacle, that space and all the following spaces should be set to 0 as we couldn't reach these places;
- For the rest spaces, once we encounter an obstacle, that space should be set to 0; otherwise, we sum up the value in its left and top and put the sum to that space. Note that the summation takes case of the cases where either or both its left and top are 0.

The space complexity is O(n) and time complexity is O(m*n).

Code:
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int> > &obstacleGrid) {
        int m=obstacleGrid.size();
        int n=obstacleGrid[0].size();
        vector<int> res(n, 0);

        // initialize using first row
        for (int j=0; j<n; j++) {
            if (obstacleGrid[0][j]==1)
                break;
            res[j]=1;
        }

        for (int i=1; i<m; i++) {
            for (int j=0; j<n; j++) {
                // assign the fist element
                if (j==0 && (obstacleGrid[i][0]==1 || res[0]==0)) {
                    res[0]=0;
                    continue;
                }
                else if (j==0 && obstacleGrid[i][0]==0) {
                    res[0]=1;
                    continue;
                }

                // the rest
                if (obstacleGrid[i][j]==0)
                    res[j]=res[j-1]+res[j];
                if (obstacleGrid[i][j]==1)
                    res[j]=0;
            }
        }
        return res[n-1];
    }
};



Sunday, June 8, 2014

[LeetCode] Unique Binary Search Trees I && II

Unique Binary Search Trees I

Problem Statement (link):
Given n, how many structurally unique BST's (binary search trees) that store values 1...n?
For example,
Given n = 3, there are a total of 5 unique BST's.
   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3
Analysis:
At first sight, this problem seems complicated. We may get some salt if we divide the problem into smaller sub-problems.

If n=0, there is 1 BST, which is the NULL tree.
If n=1, there is 1 BST, which is the single root node.
If n=2, there is 2 BST, which are BST with 1 being the root and 2 being the root.
...

We observe that for a value k, the root node may be any k=[1:n]. Thus, the number of BSTs in total is the number of sub-trees with node k valued from 1 to n.

Suppose the root node is k, then its left sub-tree consists of all nodes less that k, it's right sub-tree consists of all nodes larger than k. Then number of BSTs with root node k is then the product of number of BSTs of left sub-tree and number of BSTs of right sub-tree. Further, The number of BSTs of the left sub-tree is the number of sub-trees with the left root node valued from 1 to k-1, the number of BSTs of the right sub-tree is the number of sub-sub-trees with the right root node valued from k+1 to n. Thus we could model and solve this problem with recursion.

Furthermore, if we think twice about the method, it's sort of similar to the idea of DP - having previous calculated results stored, calculate and update new results.

Code:
class Solution {
public:
    int numTrees(int n) {
        // count[i] - number of unique BST constructed from i nodes
        vector<int> count(n+1);
        count[0] = 1;
        recur(count, n);
        return count[n];
    }

    void recur(vector<int> &count, int n) {
        for (int i=1; i<=n; i++) {
            // k - all possible num of nodes of left subtree
            for (int k=0; k<i; k++) {
                count[i] += count[k]*count[i-k-1];
            }
        }
    }
};


Unique Binary Search Trees II

Problem Statement (link):
Given n, generate all structurally unique BST's (binary search trees) that store values 1...n.
For example,
Given n = 3, your program should return all 5 unique BST's shown below.
   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3
Analysis:
This problem is a bit difficult as it's not easy to come up with a clear solution.

First, the problem asks for "all" BSTs, similar to the previous problem, we consider using DFS on every possible node values, i.e., from 1 through n.

The difficulty lies in finding a way to store each BST. The root node's value i of a valid BST must be within 1 and n, the root of its left subtree must be within 1 and i-1, the root of its right subtree must be within i+1 and n. This idea is same as that of the previous problem. Now, suppose we have all the possible nodes stored in two vectors - leftSub and rightSub, in order to construct a tree, we must construct a new root node with value i that points to a member in leftSub and points to a member in rightSub, respectively. Then, we push the node into our result vector.

Codes:
/**
 * Definition for binary tree
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<TreeNode *> generateTrees(int n) {
        vector<TreeNode*> res;
        recur(1, n, res);
        return res;
    }
    void recur(int l, int r, vector<TreeNode*> &res) {
        if (l>r) 
            res.push_back(NULL);
        else {
            for (int i=l; i<=r; i++) {
                // recursively build-up left and right subtree for root node i
                vector<TreeNode*> leftSub, rightSub;
                recur(l, i-1, leftSub);
                recur(i+1, r, rightSub);
                // Choose left and right subtree combinations for root node i
                for (int j=0; j<leftSub.size(); j++) {
                    for (int k=0; k<rightSub.size(); k++) { 
                        TreeNode* node=new TreeNode(i);
                        node->left = leftSub[j];
                        node->right= rightSub[k];
                        res.push_back(node);
                    }
                }
            }
        }
    }
};


Sunday, June 1, 2014

[LeetCode] Minimum Path Sum

Problem Statement (link):
Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right which minimizes the sum of all numbers along its path.
Note: You can only move either down or right at any point in time.
Analysis:
This problem is obviously a DPproblem. We could construct a 2D dp matrix with the same size as given grid, in which entry dp[i][j] represents the minimum path sum from (0, 0) to (i, j). After we initialize the first row and col of dp matrix, we could iteratively adding new min path sums to each entry. The last dp entry is the result we are looking for. This solution is shown in Sol 1 below. The time and space complexity of this algorithm is O(m*n), where m and n are dimension of the grid.

However, using the same idea from the problem Triangle, we could reduce the space complexity to O(n). The idea is to reuse the dp vector each time we reach a new row/col - depending on which one we choose. See Sol 2 for the implementation of this algorithm.

Code:
Sol 1:

class Solution {
public:
    int minPathSum(vector<vector<int> > &grid) {
        int m=grid.size();
        int n=grid[0].size();
        if (m==0) return 0;
        vector<vector<int>> dp(m, vector<int> (n));

        // init dp
        dp[0][0]=grid[0][0];
        for (int i=1; i<m; i++)
            dp[i][0]=grid[i][0]+dp[i-1][0];
        for (int j=1; j<n; j++)
            dp[0][j]=grid[0][j]+dp[0][j-1];

        // traverse
        for (int i=1; i<m; i++)
            for (int j=1; j<n; j++)
                dp[i][j]=min(dp[i-1][j], dp[i][j-1])+grid[i][j];
      
        return dp[m-1][n-1];
    }
};


Sol 2:

class Solution {
public:
    int minPathSum(vector<vector<int> > &grid) {
        int m=grid.size();
        int n=grid[0].size();
        if (m==0) return 0;
        vector<int> dp(n, INT_MAX);
        dp[0]=0;

        // traverse
        for (int i=0; i<m; i++) {
            dp[0]=grid[i][0]+dp[0];
            for (int j=1; j<n; j++)
                dp[j]=min(dp[j], dp[j-1])+grid[i][j];
        }
        return dp[n-1];
    }
};

Thursday, April 10, 2014

[LeetCode] Triangle

Problem Statement (link):
Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.
For example, given the following triangle
[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]
The minimum path sum from top to bottom is 11 (i.e., 2 + 3 + 5 + 1 = 11).
Note:
Bonus point if you are able to do this using only O(n) extra space, where n is the total number of rows in the triangle.
Analysis:
This problem is similar to find-shortest-path problem, it's obvious a DP.

I tried DFS first - as in Sol 1, using an int to store the smallest sum we got so far, even though it uses O(1) space, it has O(n^2) time complexity and overlapped sub-problems.

I turned to DP. It is obvious we can use a 2-D dp matrix to record all the smallest sum up to some number, where dp[i][j] record the smallest sum to j-th entry of i-th vector in triangle. However, the problem asks for O(n) space complexity.

Think about bottom-up approach instead. We use a length-n dp vector to record the smallest sum up to some number as well, where n is length of triangle. But this time, we update/re-use the dp vector iteratively as we scan through the triangle toward the pinnacle. Thus, for triangle[i], we only need the first [0 : i] of the vector.

How about the transition function? Well, except for the bottom row, where dp[i] equals each entry of the vector, we compute the new dp[i] by adding the number itself to the smallest value of its adjacent two number in the row below. We repeat this process until we reach the pinnacle. See Sol 2 for code.

Code:
Sol 1 - DFS (TLE)
int minimumTotal(vector<vector<int> > &triangle) {
    if (triangle.empty()) return 0;
    int n=triangle.size();
    //vector<int> minVec(n,INT_MAX);
    int m=INT_MAX;
    recur(triangle, m, 0, 0, 0);
    return m;
}
// i-row num; j-index in each vector
void recur(vector<vector<int>>& triangle, int& m, int val, int i, int j) {
    if (i==triangle.size()) {
        m=min(m,val);
        return;
    }
    recur(triangle, m, triangle[i][j]+val, i+1, j);
    recur(triangle, m, triangle[i][j]+val, i+1, j+1);
}

Sol 2 - DP
class Solution {
public:
    // try 2 - DP
    int minimumTotal(vector<vector<int> >&triangle) {
        if (triangle.empty()) return 0;
        vector<int> dp(triangle.size(), INT_MAX); // dp vector-reuse on every row
        // Build bottom up
        for (int i=triangle.size()-1; i>=0; i--) {
            for (int j=0; j<triangle[i].size(); j++) {
                if (i==triangle.size()-1) // The initial condition
                    dp[j]=triangle[i][j];
                else
                    dp[j]=triangle[i][j]+min(dp[j], dp[j+1]);
            }
        }
        return dp[0];
    }
};



Tuesday, April 8, 2014

[LeetCode] Word Break II

Problem Statement (link):
Given a string s and a dictionary of words dict, add spaces in s to construct a sentence where each word is a valid dictionary word.
Return all such possible sentences.
For example, given
s = "catsanddog",
dict = ["cat", "cats", "and", "sand", "dog"].
A solution is ["cats and dog", "cat sand dog"].
Analysis:
This problem requires us to return all the possible combinations with dictionary words.

The first idea is to implement a recursive DFS algorithm. In which, consider each possible prefix as a node in a n-nary tree, where n is the possible word choices starting from next index. For example, in the given example, if we construct a tree like:

                                                              ""               --> NULL string as root node
                                                          /        \
                                                   "cat"         "cats"
                                                      /                \
                                             "sand"                "and"    
                                                  /                        \
                                           "dog"                       "dog"

At each node, we need to search the entire dictionary for next possible word (child nodes).

The time complexity of this algorithm is O(m*n^2) in worst case, where n is length of string s, and m is the length of the dictionary.

I implemented this algorithm in Sol 1. However, it got TLE from OJ. How come?

If we look into the algorithm carefully, we could see there are two places that we may improve:
1) Dictionary look up duplication. In the given example, we could see that the two nodes "dog" at the last level are same. However, the DFS will do a dictionary scan each time;
2) Un-necessary look up. Suppose we had a string/sub-string s = "Leetcode", even if dictionary has no word "L" or words starting with "L", the algorithm will search next letter "e" as well.

We could borrow the DP idea from Word Break I to improve the 2) problem, where dp[i] indicates if s[i : n-1] could be constructed by dictionary words. If not, we stop search search immediately and move on. This idea is called backtracking, the dp vector here serves as the stop condition in backtracking.

The dp index map is as follows:

string:         c  a  t  s  a  n  d  d  o  g
i:                 0  1  2  3  4  5  6  7  8  9
dp:              0  1  2  3  4  5  6  7  8  9  10 --> dp[10]==true serving as the initial condition

However, I was not able to solve 1) problem.

To sum up, the time complexity of the improved algorithm is still O(m*n^2), but it saves a lot of time by stop searching earlier according to the pre-constructed dp vector.

Code:
Sol 1 - Recursive DFS:
vector<string> wordBreak(string s, unordered_set<string> &dict) {
    vector<string> dp;  // Store the out sequence
    recur(dict, dp, s, "");
    return dp;
}
void recur(unordered_set<string>& dict, vector<string>& dp, string s, string res){
    for (int i=1; i<=s.length(); i++){   // length of prefix
        if (dict.find(s.substr(0, i))!=dict.end()) {
            if (i==s.length()) {
                res+=s.substr(0, i);
                dp.push_back(res);
                return;
            }
            recur(dict, dp, s.substr(i, s.length()-i), res+s.substr(0,i)+" ");
        }
    }
    return;
}

Sol 2 - DFS + Backtracking (realized by DP):
class Solution {
public:
    // recursion + dp
    vector<string> wordBreak(string s, unordered_set<string> &dict){
        int len = s.length();
        vector<string> out;
        vector<bool> dp(len+1, false);
        // indicates if s[i, n-1] can be represented by dict
        dp[len]=true;
        for (int i=len-1; i>=0; i--) {
            if (dict.find(s.substr(i, len))!=dict.end()) {
                dp[i]=true;
                continue;
            }
            for (int j=i+1; j<len; j++) {
                if (dp[j]==true && dict.find(s.substr(i,j-i))!=dict.end()) {
                    dp[i]=true;
                }
            }
        }
        // dp + recursion
        recur(dict, dp, out, s, "", 0);
        return out;
    }

    // st-current substr start index
    void recur(unordered_set<string>& dict, vector<bool>& dp, vector<string>& out, string s, string res, int st) {
        for (int i=1; i<=s.length(); i++) {
            if (dict.find(s.substr(0, i))!=dict.end() && dp[st]==true) {
                if (i==s.length()) {
                    res+=s.substr(0, i);
                    out.push_back(res);
                    return;
                }
                recur(dict, dp, out, s.substr(i,s.length()-i), res+s.substr(0, i)+" ", st+i);
            }
        }
    }
}

Take-aways:
- Consider DFS when asked to "find all", "return all possible".
- Consider backtracking to save time - what would be the stop condition?


Monday, April 7, 2014

[LeetCode] Word Break I

Problem Statement (link):
Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated sequence of one or more dictionary words.
For example, given
s = "leetcode",
dict = ["leet", "code"].
Return true because "leetcode" can be segmented as "leet code".
Analysis:
This problem could be done naively using recursion - code given below in Sol 1. However, the time complexity is O(m^n) in worse case, where m is length of the string and n is length of the dictionary.

Obviously, the exponential solution is not optimal. We see this because the recursion has overlapped sub-problems.

Consider using DP. We construct a dp vector, where entry i represents whether s[0 : i-1] can be broke down to words in dictionary. Suppose dp[i-1] == true, dp[i] would be true iff:
1) string from index 0 to i is in dictionary, Or
2) for 0 <= j < i, if dp[j] == true, and string from index j to i is in dictionary

The index map is :

string s:         a a c d d a e
i or j:              0 1 2 3 4 5 6
dp index:    0 1 2 3 4 5 6 7

The time complexity of this DP algorithm is O(m*n), the space comlexity is O(m), where m is length of the string and n is length of the dictionary.

Code:
Sol 1 - Recursion:
bool wordBreak(string s, unordered_set<string> &dict) {
    return recur(s, dict, 0);
}
bool recur(string s, unordered_set<string> &dict, int st) {
    if (st==s.length()) return true;
    for(unordered_set<string>::iterator it=dict.begin(); it!=dict.end(); it++) {
        string t = *it;
        int len = t.length();
        if (st+len>s.length()) {
            continue;
        if (!s.substr(st, len).compare(t)) {
            if (recur(s, dict, st+len))
                return true;
        }
    }
    return false;
}

Sol 2 - DP
class Solution {
public:
    bool wordBreak(string s, unordered_set<string>& dict) {
        vector<bool> dp(s.length()+1, false);
        dp[0]=true;
        for (int i=0; i<s.length(); i++) { // i-current string index
            if (dict.find(s.substr(0,i+1))!=dict.end()) {
                dp[i+1]=true;
                continue;
            }
            for (int j=0; j<i; j++) {
                if (dp[j+1]==true && dict.find(s.substr(j+1, i-j))!=dict.end()) {
                    dp[i+1]=true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
};

Saturday, April 5, 2014

[LeetCode] Palindrome Partitioning I & II

I put these two questions in one post as the statements are quite similar, even if I solve them use different ideas.

Palindrome Partitioning I:
Problem statement (link here):
Given a string s, partition s such that every substring of the partition is a palindrome.
Return all possible palindrome partitioning of s.
For example, given s = "aab",
Return
  [
    ["aa","b"],
    ["a","a","b"]
  ]
Analysis:
My first impression is to use DP to solve this problem, we could memoize the palindromes of some substring, and build up the solution, it turned out to be not that easy :-(

I turned to a straightforward DFS recursive method with time complexity O(n^3), space complexity O(n^2).

The idea is like follows:

1, find all palindromes in substring s[0], and all the substrings in s[1:end]
2, find all palindromes in substring s[0:1], and all the substrings in s[2:end]
...
n, find all palindromes in substring s[0:end-1], and all the substrings in s[end]

There are further three things to remember:
1, stop condition: we need to stop when we reach the end in a search;
2, for loop: in each search, we start at the next index of previous found palindrome till end of the entire string;
3, use vector: remember to pop_back();

Code:
class Solution {
public:
    vector<vector<string>> partition(string s) {
        vector<vector<string> > out;
        vector<string> row;
        recur(out, row, s, 0);
        return out;
    }
    void recur(vector<vector<string>>& out, vector<string>& row, string s, int st) {
        if (st==s.size()) {
            out.push_back(row);
            return;
        }
        for (int i=st; i<s.size(); i++) {   // i is the end index
            if (isPalindrome(s, st, i)) {
                row.push_back(s.substr(st, i-st+1));
                recur(out, row, s, i+1);
                row.pop_back();
            }
        }
    }
    bool isPalindrome(string s, int st, int ed) {
        while (st<=ed) {
            if (s[st]==s[ed]) {
                st++; ed--;
            }
            else return false;
        }
        return true;
    }
};

Takeaways:
When you see "return all", "find all possible", "find the total number of", the idea is usually DFS recursive algorithm.

Palindrome Partitioning II:
Problem statement (link here):
Given a string s, partition s such that every substring of the partition is a palindrome.
Return the minimum cuts needed for a palindrome partitioning of s.
For example, given s = "aab",
Return 1 since the palindrome partitioning ["aa","b"] could be produced using 1 cut.
Analysis:
We could use the same approach as in Palindrom Partitioning I, which is to recursively search all possibilities and find the minimum number of cuts. However, this naive approach would fail the time test.

Apparently, we should think about how to use DP to solve this problem.

First of all, we will need a DP array to store the number of min-cuts, as we need one extra space for initialization, this dp array should have length (n+1), where n is the length of string s. If we construct the dp array from the end of string s, then dp[i] represents the number of minimum cuts of substring s[i: n-1].

How would we find the min-cuts between i and n-1? Consider the following example,

string:       a   b   a   a   c   a   b   a   b   a   c   c   d   a
str index:  0                       i         j     j+1                    n-1
dp index:  0                       i         j     j+1                    n-1   n

We partition substring s[i: n-1] into two sub-substrings s[i: j] and s[j+1: n-1]. For dp[i], we compare its original dp[i] value with new cut after index j - dp[j+1] + 1, the "1" stands for the one cut cost for new palindrome s[i: j].

Thus, we have the transition function: dp[i] = min(dp[i], dp[j+1] + 1).

Next, consider how we determine a palindrome. In Palindrome Partitioning I, we iteratively check two chars from begin and end, begin-1 and end-1, ... However, if we use a bool 2D DP matrix to record if substring s[i+1: j-1] is a valid substring, and compare only s[i] and s[j] for substring s[i: j], we significantly reduce the number of comparisons. More precisely, we bring down the time complexity with a factor of n.

In sum, the algorithm requires O(n^2) time complexity and O(n^2) space complexity.

Code:
class Solution {
public:
    int minCut(string s) {
        int len=s.length();
        // whether substring between i & j form a palindrome
        bool isPalind[len][len];
        // num of min-cuts from i to n
        int dp[len+1];
        // set initial values
        for (int i=0; i<len; i++)
            for (int j=0; j<len; j++)
                isPalind[i][j]=false;
        // worst case - cut every char
        for (int i=0; i<=len; i++)
            dp[i]=len-i;
        // construct DP array
        for (int i=len-1; i>=0; i--) {
            for (int j=i; j<len; j++) {
                if (s[i]==s[j] && (j-i<2 || isPalind[i+1][j-1])) {
                    isPalind[i][j]=true;
                    dp[i]=min(dp[i],dp[j+1]+1);
                }
            }
        }
        return dp[0]-1;
    }
};

Takeaways:
We should consider DP when asked to find the "optimal" solution.

Friday, April 4, 2014

[LeetCode] Edit Distance

Problem statement (link here):

Given two words word1 and word2, find the minimum number of steps required to convert word1 to word2. (each operation is counted as 1 step.)
You have the following 3 operations permitted on a word:
a) Insert a character
b) Delete a character
c) Replace a character

Analysis:
This is a classic DP problem. The reason that DP would reduce time complexity is that this problem has overlapped sub-problems, i.e., given that we found edit distance of length-i words, if we want to further find the edit distance of length-j words, where j>i, we would only need to work on (j-i) portion instead of processing from the very beginning.

We construct a (m+1) x (n+1) 2D matrix with each entry represents a character from each word, where m, n represents the length of each word, the extra 1 space in each dimension is to represent the empty char.

e.g., string word1 = "rabb"; string word2 = "rac", we construct the matrix as follows, matrix entry [i][j] represents the min number of steps to change word1[:i] to word2[:j]

       dp =    _ | 0 | r  | a | b | b |
                  0 | 0 | 1 | 2 | 3 | 4 |
                  r  | 1 | 0 | 1 | 2 | 3 |
                  a | 2 | 1 | 0 | 1 | 2 |
                  c | 3 | 2 | 1 | 1 | 2 |

To recall, the three operations we have are: a) insert; b) delete; c) replace. Each operation costs 1 step.

If two chars are same, i.e., word1[i-1]==word2[j-1], we don't need to update dp[i][j], as dp[i][j] = dp[i-1][j-1];
If two chars are different, we can choose either of the three operations
- if we choose to insert a char into word1, the cost is dp[i][j-1] as after insertion we will compare the first (j-1) chars of word2 with word1, i.e., dp[i][j] = dp[i][j-1] + 1;
- if we choose to delete the current char from word1, the cost is dp[i-1][j] as after deletion we will compare the first (i-1) chars of word1 with word2, i.e., dp[i][j] = dp[i-1][j] + 1;
- if we choose to replace the current char in word1, the cost is dp[i][j] as after replacement we will compare the first (i-1) chars of word1 with the first (j-1) chars of word2, i.e., dp[i][j] = dp[i-1][j-1] + 1;
Then we choose the min cost of these three operations.

This process takes place iteratively through all chars in each words.

Code:
class Solution {
public:
    int minDistance(string word1, string word2) {
        if (word1==word2) return 0;
        vector<vector<int> > dp(word1.length()+1, vector<int>(word2.length()+1, 0));
        // takes care of first row and first col
        for (int i=1; i<=word1.length(); i++)
            dp[i][0]=i;
        for (int j=1; j<=word2.length(); j++)
            dp[0][j]=j;
        // iteration starts
        for (int i=1; i<=word1.length(); i++) {
            for (int j=1; j<=word2.length(); j++) {
                if (word1[i-1]==word2[j-1])
                    dp[i][j]=dp[i-1][j-1];
                else
                    dp[i][j]=min(dp[i][j-1], min(dp[i-1][j], dp[i-1][j-1]))+1;    // cost of insert, delete, replace
            }
        }
        return dp[word1.length()][word2.length()];
    }
};

Obviously, as we need build this 2D table, the time complexity of this algorithm is O(m*n), where m and n are the length of the two words. The space complexity is also O(m*n).

We could use one vector instead of one matrix to store the DP result, this potentially brings space complexity back to linear.