Tag Archives: DP

LeetCode Maximum Subarray

53. Maximum Subarray

Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.

Example:

Input: [-2,1,-3,4,-1,2,1,-5,4],
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

Follow up:

If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.


求最大连续子数组和,这是一道非常经典的题目,从本科时候就开始做了。有两种解法。

动态规划法

用dp数组存储到当前元素的最大子数组和,如dp[i-1]表示包含第i-1个元素的最大子数组和,则dp[i]表示包含第i个元素的最大子数组和。如果dp[i-1]>0,则dp[i]=dp[i-1]+nums[i];否则dp[i]=nums[i]。完整代码如下:

class Solution {
public:
    int maxSubArray(vector<int>& nums)
    {
        vector<int> dp(nums.size(), 0);
        dp[0] = nums[0];
        int ans = nums[0];
        for (int i = 1; i < nums.size(); i++) {
            if (dp[i – 1] > 0)
                dp[i] = dp[i – 1] + nums[i];
            else
                dp[i] = nums[i];
            if (dp[i] > ans)
                ans = dp[i];
        }
        return ans;
    }
};

本代码只用扫描一遍数组,所以时间复杂度为$O(n)$。本代码提交AC,用时12MS。

分治法

将数组一分为二,比如数组[a,b]分为[a,m-1]和[m+1,b],递归求解[a,m-1]和[m+1,b]的最大连续子数组和lmax和rmax,但是[a,b]的最大连续子数组和还可能是跨过中点m的,所以还应该从m往前和往后求一个包含m的最大连续子数组和mmax,然后再求lmax,rmax,mmax的最大值。完整代码如下:

class Solution {
public:
    int divide(vector<int>& nums, int left, int right)
    {
        if (left == right)
            return nums[left];
        if (left == right – 1)
            return max(nums[left] + nums[right], max(nums[left], nums[right]));
        int mid = (left + right) / 2;
        int lmax = divide(nums, left, mid – 1);
        int rmax = divide(nums, mid + 1, right);
        int mmax = nums[mid], tmp = nums[mid];
        for (int i = mid – 1; i >= left; i–) {
            tmp += nums[i];
            if (tmp > mmax)
                mmax = tmp;
        }
        tmp = mmax;
        for (int i = mid + 1; i <= right; i++) {
            tmp += nums[i];
            if (tmp > mmax)
                mmax = tmp;
        }
        return max(mmax, max(lmax, rmax));
    }
    int maxSubArray(vector<int>& nums) { return divide(nums, 0, nums.size() – 1); }
};

本代码可以类比平面上求最近点对的例子,需要考虑跨界的问题,时间复杂度为$O(nlgn)$。本代码提交AC,用时也是12MS。

综合来看,还是动态规划法更漂亮。

二刷。动态规划还可以再简化,即不用额外的DP数组,代码如下:

class Solution {
public:
	int maxSubArray(vector<int>& nums) {
		int n = nums.size();
		int max_sum = INT_MIN, cur_sum = 0;
		for (int i = 0; i < n; ++i) {
			if (cur_sum >= 0) {
				cur_sum += nums[i];
			}
			else {
				cur_sum = nums[i];
			}
			max_sum = max(max_sum, cur_sum);
		}
		return max_sum;
	}
};

本代码提交AC,用时4MS,击败98%用户。

LeetCode Longest Valid Parentheses

32. Longest Valid Parentheses

Given a string containing just the characters '(' and ')', find the length of the longest valid (well-formed) parentheses substring.

Example 1:

Input: "(()"
Output: 2
Explanation: The longest valid parentheses substring is "()"

Example 2:

Input: ")()())"
Output: 4
Explanation: The longest valid parentheses substring is "()()"

此题要求一个括号字符串中最长的合法的括号串的长度。 遇到括号问题,想当然的用栈来解决,可以尝试每一个以'(‘开头的字符串,用栈求出其最长合法括号串的长度,然后求全局最大值,如下:

class Solution {
public:
    int longestValidParentheses(string s)
    {
        int n = s.size(), maxval = 0;
        for (int i = 0; i < n; i++) { if (s[i] == ‘(‘) {
                stack<char> sc;
                sc.push(s[i]);
                int cur = 0;
                for (int j = i + 1; j < n; j++) {
                    char t = sc.top();
                    if (s[j] == ‘)’)
                        { if (t == ‘(‘) {
                                sc.pop();
                                cur++; } else break;
                        }
                    sc.push(s[j]);
                }
                if (cur > maxval)
                    maxval = cur; }
        }
        return maxval * 2;
    }
};

但是这个算法是$O(n^2)$的,TLE。

后来看题解,居然有$O(n)$的解法,厉害,如下:

class Solution {
public:
	int longestValidParentheses(string s)
	{
		int n = s.size(), maxval = 0, start = -1; // start初始化为-1
		stack<int> si;
		for (int i = 0; i < n; i++) {
			if (s[i] == '(') {
				si.push(i); //左括号无条件压栈 
			} else { //右括号不压栈
				if (si.empty()) {
					start = i; //以右括号作为新的起点
				} else {
					si.pop();
					if (si.empty()) // ()()() or (()())
						maxval = max(maxval, i - start);
					else // (() 用当前栈顶索引作为起点
						maxval = max(maxval, i - si.top());
				}
			}
		}
		return maxval;
	}
};

本算法提交AC,用时12MS。

它也是用堆栈,但是堆栈里存的不是括号,而是括号的索引。左括号无条件压栈,右括号不压栈,只是作为计算括号对的长度的起点(其实起点应该是右括号索引+1,但是计算长度的时候用右括号索引更方便)。

所以栈里面只有左括号,当右括号来了,且栈为空时,说明之前已经配对完了,要开始新的配对,所以设置新的起点。

如果栈不为空,则pop。如果pop完之后栈为空,也说明配对完成,可以更新最佳长度;如果pop完之后栈里还有左括号,则是(((()这种类型,左括号更多,则匹配的起点应该是栈顶的索引,而不是之前设置的右括号索引+1,如下:

(((() ^ ^

二刷。比较容易理解的是动态规划的方法。设dp[i]表示以字符i结尾的最长合法括号串的长度,则所有左括号对应的dp[i]都等于0,因为合法的括号串都是以右括号结尾的。

如果s[i]==’)’,有两种情况。第一种情况是s[i-1]='(‘,则s[i]和s[i-1]配对,所以dp[i]=dp[i-2]+2。第二种情况是s[i-1]也=’)’,则s[i]无法和s[i-1]配对,但是如果向前跨过s[i-1]对应的合法字符串后有左括号出现,则s[i]有望作为一个外围的右大括号配对成功,如下图所示,最右边的是第s[i]=’)’,向前跨过了(xxx),和前一个左括号配对成功。这种情况下,dp[i]=dp[i-1]+2,又因为配对的左括号前面还有可能是合法的括号串,所以还要加上dp[i-dp[i-1]-1-1]。

xxx((xxxx))

完整代码如下:

class Solution {
public:
	int longestValidParentheses(string s) {
		int n = s.size();
		vector<int> dp(n, 0);
		int ans = 0;
		for (int i = 0; i < n; ++i) {
			if (s[i] == ')') {
				if (i > 0 && s[i - 1] == '(') {
					dp[i] += 2;
					if (i >= 2)dp[i] += dp[i - 2];
				}
				else if (i > 0 && s[i - 1] == ')') {
					int left = i - dp[i - 1] - 1;
					if (left >= 0 && s[left] == '(') {
						dp[i] = dp[i - 1] + 2;
						if (left > 0)dp[i] += dp[left - 1];
					}
				}
			}
			ans = max(ans, dp[i]);
		}
		return ans;
	}
};

本代码提交AC,用时4MS。

至于用栈的方法,是这样的:栈中只插入左括号的下标,以便来了右括号后,把配对的左括号弹出,剩下栈顶左括号下标作为之前弹出的合法括号串的起始位置。如果栈顶为空,且来了右括号,则这个右括号肯定配对失败,所以也把它的下标入栈,用来充当后续合法括号串的起始位置。

更简洁易懂的代码如下:

class Solution {
public:
	int longestValidParentheses(string s) {
		int n = s.size();
		int ans = 0;
		stack<int> stk;
		stk.push(-1);
		for (int i = 0; i < n; ++i) {
			if (s[i] == '(') {
				stk.push(i);
			}
			else {
				if (!stk.empty())stk.pop();
				if (stk.empty()) {
					stk.push(i);
				}
				else {
					ans = max(ans, i - stk.top());
				}
			}
		}
		return ans;
	}
};

本代码提交AC,用时4MS。

LeetCode Decode Ways

91. Decode Ways

A message containing letters from A-Z is being encoded to numbers using the following mapping:

'A' -> 1
'B' -> 2
...
'Z' -> 26

Given a non-empty string containing only digits, determine the total number of ways to decode it.

Example 1:

Input: "12"
Output: 2
Explanation: It could be decoded as "AB" (1 2) or "L" (12).

Example 2:

Input: "226"
Output: 3
Explanation: It could be decoded as "BZ" (2 26), "VF" (22 6), or "BBF" (2 2 6).

动态规划题。设s为加密字符串,dp[i]表示s[1,…,i]解密的方案数。如果$s[i]\in [1,9]$,则s[i]可以单独解密,有dp[i]+=dp[i-1];如果$s[i-1]s[i]\in [10,26]$,则s[i]可以和s[i-1]拼起来一起解密,有dp[i]+=dp[i-2]。
DP转移公式如下:
$$\begin{cases}dp[i] += dp[i-1] & \text{if $s[i] \in [1,9]$}\\dp[i] += dp[i-2] & \text{if $s[i-1]s[i] \in [10,26]$}\\\end{cases}$$
注意如果0出现在首位则无法解密。
完整代码如下:

class Solution {
public:
    int numDecodings(string s)
    {
        if (s == "" || s[0] == ‘0’)
            return 0;
        s = "^" + s;
        int n = s.size();
        vector<int> dp(n, 0);
        dp[0] = dp[1] = 1;
        for (int i = 2; i < n; i++) {
            if (s[i] >= ‘1’ && s[i] <= ‘9’)
                dp[i] += dp[i – 1];
            if ((s[i – 1] == ‘1’ && s[i] >= ‘0’ && s[i] <= ‘9’) || (s[i – 1] == ‘2’ && s[i] >= ‘0’ && s[i] <= ‘6’))
                dp[i] += dp[i – 2];
        }
        return dp[n – 1];
    }
};

本代码提交AC,用时8MS。

LeetCode Distinct Subsequences

115. Distinct Subsequences

Given a string S and a string T, count the number of distinct subsequences of S which equals T.

A subsequence of a string is a new string which is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (ie, "ACE" is a subsequence of "ABCDE" while "AEC" is not).

It’s guaranteed the answer fits on a 32-bit signed integer.

Example 1:

Input: S = "rabbbit", T = "rabbit"
Output: 3
Explanation:
As shown below, there are 3 ways you can generate "rabbit" from S.
(The caret symbol ^ means the chosen letters)

rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^

Example 2:

Input: S = "babgbag", T = "bag"
Output: 5
Explanation:
As shown below, there are 5 ways you can generate "bag" from S.
(The caret symbol ^ means the chosen letters)

babgbag
^^ ^
babgbag
^^    ^
babgbag
^    ^^
babgbag
  ^  ^^
babgbag
    ^^^

这问题描述不清,题目想问S中有多少个不同的subsequence等于T,这里的不同是指形成subsequence的方法不同,比如样例中,rabbbit可以去掉中间任意一个b都能形成T,所以不同的方法有3种。 这是典型的动态规划问题。假设dp[i][j]表示由S[1,…,i]变到T[1,…,j]有多少种subsequence方法。此时我们有两种选择,如果S[i]!=T[j],则等价于由S[1,…,i-1]变到T[1,…,j],所以dp[i][j]=dp[i-1][j];如果S[i]==T[j],则还可以由S[1,…,i-1]变到T[1,…,j-1],所以dp[i][j]+=dp[i-1][j-1]。
DP转移公式如下:
$$dp[i][j]=\begin{cases}dp[i-1][j] & \text{if $S[i]\neq T[j]$}\\dp[i-1][j]+dp[i-1][j-1] & \text{if $S[i]=T[j]$}\\\end{cases}$$
为了便于理解,我们先在s和t的开头添加一个哨兵字符’^’,初始值dp[i][0]=1,因为由任意字符串转换为空字符串只有一种方法,就是把s中的字符全删了;dp[0][j]=0,因为空字符串的subsequence不可能是是某个非空字符串。
完整代码如下:

class Solution {
public:
    int numDistinct(string s, string t)
    {
        if (s == "")
            return 0;
        if (t == "")
            return 1;
        s = "^" + s;
        t = "^" + t;
        int n = s.size(), m = t.size();
        vector<vector<int> > dp(n, vector<int>(m, 0));
        for (int i = 0; i < n; i++)
            dp[i][0] = 1; // ‘*’ -> ” 1 sub
        for (int i = 1; i < n; i++) {
            for (int j = 1; j < m; j++) {
                dp[i][j] = dp[i – 1][j];
                if (s[i] == t[j])
                    dp[i][j] += dp[i – 1][j – 1];
            }
        }
        return dp[n – 1][m – 1];
    }
};

本代码提交AC,用时20MS。

二刷。不添加哨兵,vector增加1也是可以的,代码如下:

class Solution {
public:
    int numDistinct(string s, string t) {
        int m = s.size(), n = t.size();
        if(n > m) return 0;
        vector<vector<long long>> dp(m + 1, vector<long long>(n + 1, 0));
        for(int i = 0; i < m; ++i) dp[i][0] = 1;
        for(int i = 1; i <= m; ++i) {
            for(int j = 1; j <= n; ++j) {
                if(s[i - 1] == t[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[m][n];
    }
};

本代码提交AC,用时20MS。

LeetCode Palindrome Partitioning II

132. Palindrome Partitioning II

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.

Example:

Input: "aab"
Output: 1
Explanation: The palindrome partitioning ["aa","b"] could be produced using 1 cut.

动态规划题目,类似于矩阵链相乘。设dp[i][j]表示s[i,…,j]是否为回文串,cut[j]表示s[1,…,j]需要多少刀才能切成回文串。动态规划运行到最后返回cut[n]。
对于cut[j],切第一刀的时候,我们尝试前面j个切割位点,比如我们尝试切第i点,则s[1,…,j]被分成了s[1,…,i-1]和s[i,…,j],如果s[i,…,j]为回文串,则s[i,…,j]不需要再切了,又因为cut[i-1]我们之前已经算过了,所以在i处切一刀,有cut[j]=cut[i-1]+1。尝试所有的切割位点i,找最小的cut[j]。
如果发现i=1的时候s[i,…,j]为回文串,则s[1,…,j]不需要再切了,所以cut[j]=0。
完整代码如下:

class Solution {
public:
    int minCut(string s)
    {
        int n = s.size();
        vector<vector<bool> > dp(n, vector<bool>(n, false));
        for (int i = 0; i < n; i++)
            dp[i][i] = true;
        vector<int> cut(n, 0); // # of cut for s[0,j]
        for (int j = 0; j < n; j++) {
            cut[j] = j; // set maximum # of cut
            for (int i = 0; i <= j; i++) {
                if (s[i] == s[j] && (j – i <= 1 || dp[i + 1][j – 1])) {
                    dp[i][j] = true;
                    if (i > 0) // if need to cut, add 1 to the previous cut[i-1]
                        cut[j] = min(cut[j], cut[i – 1] + 1);
                    else // if [0…j] is palindrome, no need to cut cut[j] = 0;
                }
            }
        }
        return cut[n – 1];
    }
};

本代码提交AC,用时84MS。

二刷。参考讨论区一个大神的做法:https://discuss.leetcode.com/topic/2840/my-solution-does-not-need-a-table-for-palindrome-is-it-right-it-uses-only-o-n-space。这种方法不需要上面的做法的dp数组,效率也会高很多。

还是需要cuts数组,cuts[i]表示前i个字符切成回文子串,最少的刀数。对于每一个字符,我们枚举以该字符为中心,能够扩展到的最长的回文子串的长度,比如下图,以b为中心扩展,扩展到aba是一个回文子串,那么要把Y切成回文子串,需要的刀数不超过把X切成回文子串需要的刀数加1,也就是cuts[Y]=min(cuts[Y],cuts[X]+1)。
注意,扩展的时候,需要考虑到以b为中心的回文子串的长度是奇数还是偶数。

.......aba...
|<-X->| ^
|<---Y-->|

通过枚举每一个回文中心点,我们可以算到所有的cuts,最后返回cuts[n]。完整代码如下:

class Solution {
public:
    int minCut(string s)
    {
        int n = s.size();
        vector<int> cuts(n + 1, 0); // cuts[i] 表示前i个字符切成回文子串,最少的刀数
        for (int i = 0; i <= n; ++i)
            cuts[i] = i – 1;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; i – j >= 0 && i + j < n && s[i – j] == s[i + j]; ++j) // 最后一个是奇数长回文子串
                cuts[i + j + 1] = min(cuts[i + j + 1], cuts[i – j] + 1);
            for (int j = 1; i – j + 1 >= 0 && i + j < n && s[i – j + 1] == s[i + j]; ++j) // 最后一个是偶数长回文子串
                cuts[i + j + 1] = min(cuts[i + j + 1], cuts[i – j + 1] + 1);
        }
        return cuts[n];
    }
};

本代码提交AC,用时6MS,神速。

三刷。两个DP,第一个DP求解出任意两个位置的子串[i:j]是否为回文。第二个DP2,DP2[j]表示前j长的字符串,需要的最小cut数,对于第j个字符,它可以自己独立成为一个回文,所以DP2[j]=DP2[j-1]+1;还可以遍历j前面的字符i,如果[i:j]能形成回文的话,则DP2[j]=DP2[i-1]+1。完整代码如下:

class Solution {
public:
	void preprocess(const string &s, vector<vector<int>> &dp) {
		int n = s.size();
		for (int i = 0; i < n; ++i)dp[i][i] = 1;
		for (int len = 2; len <= n; ++len) {
			for (int i = 0; i < n; ++i) {
				int j = i + len - 1;
				if (j < n && s[i] == s[j] && (len == 2 || dp[i + 1][j - 1] == 1)) {
					dp[i][j] = 1;
				}
			}
		}
	}
	int minCut(string s) {
		int n = s.size();
		if (n == 0 || n == 1)return 0;
		vector<vector<int>> dp(n, vector<int>(n, 0));
		preprocess(s, dp);
		vector<int> dp2(n, 0);
		for (int j = 1; j < n; ++j) {
			dp2[j] = dp2[j - 1] + 1;
			for (int i = j - 1; i >= 0; --i) {
				if (dp[i][j] == 1) {
					int pre_cut = i > 0 ? dp2[i - 1] + 1 : 0;
					dp2[j] = min(dp2[j], pre_cut);
				}
			}
		}
		return dp2[n - 1];
	}
};

本代码提交AC,用时44MS。

LeetCode Longest Palindromic Substring

5. Longest Palindromic Substring

Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.

Example 1:

Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.

Example 2:

Input: "cbbd"
Output: "bb"

这题要求我们找出一个字符串S中的最长回文子串,之前在hihoCoder 1032-最长回文子串就详细讨论过相关问题,这里就不再赘述了,代码完全一样,如下:

class Solution {
public:
	string preProcess(string s)
	{
		int n = s.size();
		if (n == 0)
			return "^#";
		string ret = "^";
		for (int i = 0; i < n; i++)
			ret += "#" + s.substr(i, 1);
		return ret + "#$";
	}
	string longestPalindrome(string s) {
		string t = preProcess(s);
		vector<int> p(t.size(), 0);
		int c = 0, r = 0, n = t.size();
		for (int i = 1; i < n - 1; i++)
		{
			int i_mirror = 2 * c - i;
			p[i] = (r > i) ? min(p[i_mirror], r - i) : 0;
			while (t[i + p[i] + 1] == t[i - p[i] - 1])
				p[i]++;
			if (i + p[i] > r)
			{
				c = i;
				r = i + p[i];
			}
		}
		
		int maxLen = 0, centerIndex = 0;
		for (int i = 1; i < n - 1; i++)
		{
			if (p[i] > maxLen)
			{
				maxLen = p[i];
				centerIndex = i;
			}
		}
		
		return s.substr((centerIndex - 1 - maxLen) / 2, maxLen);
	}
};

本代码提交AC,用时12MS,击败了73%的CPP用户,看来效率不错,估计中心扩展法$O(n^2)$也能过。

二刷。上面的Manacher’s算法过于小众,咱们还是来点大众算法吧,容易理解和记忆。

首先是DP方法,设dp[i][j]=1表示s[i:j]形成回文,=0表示不是回文。则如果s[i:j]形成回文,且s[i-1]==s[j+1],则可以在s[i:j]的基础上进一步扩展,使得s[i-1:j+1]也是回文,即dp[i-1][j+1]=1。

稍微要注意的是DP的时候,第一层循环是子串的长度,因为我们是在短的子串上看起周围2个字符是否相等,所以先要求出短的dp[i][j],再求长的dp[i-1][j+1]。

完整代码如下:

class Solution {
public:
	string longestPalindrome(string s) {
		int n = s.size();
		if (n == 0)return "";
		vector<vector<int>> dp(n, vector<int>(n, 0));
		int ans = 1, u = 0, v = 0;
		for (int len = 0; len < n; ++len) {
			for (int i = 0; i < n; ++i) {
				int j = i + len;
				if (j >= n)break;

				if (len == 0)dp[i][j] = 1;
				else if (len == 1) {
					if (s[i] == s[j]) {
						dp[i][j] = 1;
						if (2 > ans) {
							ans = 2;
							u = i;
							v = j;
						}
					}
				}
				else {
					if (dp[i + 1][j - 1] == 1 && s[i] == s[j]) {
						dp[i][j] = 1;
						if (j - i + 1 > ans) {
							ans = j - i + 1;
							u = i;
							v = j;
						}
					}
				}
			}
		}
		return s.substr(u, v - u + 1);
	}
};

本代码提交AC,用时184MS。

还有一种中心扩展法也很好理解,即假设每个字符是回文的中心字符,然后看看两端字符是否相等,如果相等的话,则继续扩展。需要注意的是,回文有两种形式,一种是aba,即中心只有一个字符,且总长度是奇数;另一种是abba,即中心有两个字符,且总长度是偶数。这种解法相比于上一种解法,只需要O(1)的空间复杂度。

完整代码如下:

class Solution {
public:
	int expand(string &s, int l, int r) {
		int n = s.size();
		while (l >= 0 && r < n&&s[l] == s[r]) {
			--l;
			++r;
		}
		return r - l - 1;
	}
	string longestPalindrome(string s) {
		if (s.size() == 0)return "";
		string ans = "";
		for (int i = 0; i < s.size(); ++i) {
			int len1= expand(s, i, i);
			int len2= expand(s, i, i + 1);
			if (len1 > len2&&len1 > ans.size()) {
				ans = s.substr(i - len1 / 2, len1);
			}
			if (len2 > len1&&len2 > ans.size()) {
				ans = s.substr(i - len2 / 2 + 1, len2);
			}
		}
		return ans;
	}
};

本代码提交AC,用时20MS。

HDOJ 5418-Victor and World

HDOJ 5418-Victor and World Victor and World Time Limit: 4000/2000 MS (Java/Others) Memory Limit: 262144/131072 K (Java/Others) Total Submission(s): 643 Accepted Submission(s): 268 Problem Description After trying hard for many years, Victor has finally received a pilot license. To have a celebration, he intends to buy himself an airplane and fly around the world. There are n countries on the earth, which are numbered from 1 to n. They are connected by m undirected flights, detailedly the i-th flight connects the ui-th and the vi-th country, and it will cost Victor’s airplane wi L fuel if Victor flies through it. And it is possible for him to fly to every country from the first country. Victor now is at the country whose number is 1, he wants to know the minimal amount of fuel for him to visit every country at least once and finally return to the first country. Input The first line of the input contains an integer T, denoting the number of test cases. In every test case, there are two integers n and m in the first line, denoting the number of the countries and the number of the flights. Then there are m lines, each line contains three integers ui, vi and wi, describing a flight. 1≤T≤20. 1≤n≤16. 1≤m≤100000. 1≤wi≤100. 1≤ui,vi≤n. Output Your program should print T lines : the i-th of these should contain a single integer, denoting the minimal amount of fuel for Victor to finish the travel. Sample Input 1 3 2 1 2 2 1 3 3 Sample Output 10 Source BestCoder Round #52 (div.2) Recommend hujie | We have carefully selected several similar problems for you: 5421 5420 5419 5416 5415


此题是可重复访问城市的旅行商问题(TSP),即从1号城市开始,经过每个城市至少一次,然后回到1号,问飞机消耗的最少燃料是多少。 根据题解,首先使用Floyd算法求出所有点对的最短路径dis[i][j],然后DP算法求解。 用一个整数s的二进制表示当前走过城市的状态,第i位为1表示第i个城市已经访问过,为0表示没访问过。dp[s][i]表示当前走过城市状态为s,且最后走过的城市编号为i,即到达了城市i。则有状态转移方程: $$dp[s|(1<<i)][i]=min(dp[s][j]+dis[i][j])$$ 条件为s&(1<<j)!=0且s&(1<<i)==0。 含义为:在s已经访问过j但是没有访问过i的情况下,转移到访问i的状态s|(1<<i)消耗的燃料为所有“s最后访问的是j消耗的燃料dp[s][j],加上从j走到i消耗的燃料之和”情况中的最小值。 最终结果就是s中所有位为1的状态,再从该状态回到城市1的燃料之和的最小值,即min(dp[(1<<n)-1][i]+dis[i][1])。 完整代码如下: [cpp] #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int kMaxN = 17, kINF = 0x3f3f3f3f; int dis[kMaxN][kMaxN], dp[1 << kMaxN][kMaxN]; int n, m; void Init() { memset(dis, 0x3f, sizeof(dis)); memset(dp, 0x3f, sizeof(dp)); for (int i = 1; i <= n; i++) dis[i][i] = 0; } void Floyd() { for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) dis[i][j] = min(dis[i][k] + dis[k][j], dis[i][j]); } int main() { freopen("input.txt", "r", stdin); int t, u, v, w; scanf("%d", &t); while (t–) { scanf("%d %d", &n, &m); Init(); while (m–) { scanf("%d %d %d", &u, &v, &w); dis[u][v] = min(w, dis[u][v]); dis[v][u] = dis[u][v]; } Floyd(); dp[1][1] = 0; for (int status = 1; status < (1 << n); status++)//枚举状态 { for (int i = 1; i <= n; i++)//枚举最后访问的城市 { //if (status&(1 << (i-1))==0)//(1) if ((status&(1 << (i-1)))==0)//(2)i有没有访问过都可以 { for (int j = 1; j <= n; j++)//枚举中间转折点 { if (status&(1 << (j-1)))//(3)转折点j一定要访问过 dp[status | (1 << (i-1))][i] = min(dp[status | (1 << (i-1))][i], dp[status][j] + dis[i][j]); } } } } int ans = kINF; for (int i = 1; i <= n; i++) ans = min(ans, dp[(1 << n) – 1][i] + dis[i][1]); printf("%d\n", n == 1 ? 0 : ans); } return 0; } [/cpp] 本代码提交AC,用时717MS,内存10284K。 代码中需要注意一点,代码(2)一定不要写成了(1),要不然WA到死,&的优先级低于==,切记! 其实(2)处的if可以不要,因为本TSP问题可以重复访问某个城市,所以这里可以不判断下一步需要访问的城市之前有没有访问过,只是加了if能缩短时间;但是(3)处的if判断一定要,因为需要在城市j中转,j之前必须访问过。 另外查看大神的代码,发现很喜欢用0x3f3f3f3f而不是0x7fffffff作为程序中的无穷大,这是有道理的,详情看这篇博客。]]>

hihoCoder 1124-好矩阵

hihoCoder 1124-好矩阵 #1124 : 好矩阵 时间限制:3000ms 单点时限:1000ms 内存限制:256MB 描述 给定n, m。一个n × m矩阵是好矩阵当且仅当它的每个位置都是非负整数,且每行每列的和 ≤ 2。求好矩阵的个数,模$$10^9$$ + 7 输入 第一行一个整数T,表示测试点个数。下面T个测试点。每个测试点一行,包含两个整数n,m。1 ≤ T ≤ $$10^4$$. 1 ≤ n, m ≤ 100. 输出 T行。每行是对应测试点的答案。 样例输入 1 2 2 样例输出 26


这一题的题意也很明确,如果是单个测试数据,可以考虑用DFS,但是本题测试数据最多有$$10^4$$,当n和m不同时,DFS的结果不能复用,所以超时妥妥的。 要想中间结果复用,一定是DP,所以考虑用DP来做,我是参考了题解此博客。 完整代码如下: [cpp] #include<iostream> #include<cstdio> using namespace std; const int kMaxMN = 105, kMod = 1000000007; typedef long long ll; ll f[kMaxMN][kMaxMN][kMaxMN]; ll ans[kMaxMN][kMaxMN]; void Init() { for (int m = 1; m < kMaxMN; m++) { for (int n = 0; n < kMaxMN; n++) for (int a = 0; a <= m; a++) for (int b = 0; a + b <= m; b++) f[n][a][b] = 0; f[0][m][0] = 1; for (int n = 1; n < kMaxMN; n++) { //f[n][m][0] = 1;//如果f[n-1][m][0]=0,即不存在全为0的情况,则f[n][m][0]不等于1而是等于0 for (int a = 0; a <= m; a++) { for (int b = 0; a + b <= m; b++) { f[n][a][b] += f[n – 1][a][b];//(1) f[n][a][b] %= kMod; if (a > 0) { //(2.1) f[n][a – 1][b + 1] += (ll)a*f[n – 1][a][b]; f[n][a – 1][b + 1] %= kMod; //(4) f[n][a – 1][b] += (ll)a*f[n – 1][a][b]; f[n][a – 1][b] %= kMod; } if (b > 0)//(2.2) { f[n][a][b – 1] += (ll)b*f[n – 1][a][b]; f[n][a][b – 1] %= kMod; } if (a > 1)//3.1 { f[n][a – 2][b + 2] += (ll)(a*(a – 1) / 2)*f[n – 1][a][b]; f[n][a – 2][b + 2] %= kMod; } if (a > 0 && b > 0)//3.2 { f[n][a – 1][b] += (ll)a*(ll)b*f[n – 1][a][b]; f[n][a – 1][b] %= kMod; } if (b > 1)//3.3 { f[n][a][b – 2] += (ll)(b*(b – 1) / 2)*f[n – 1][a][b]; f[n][a][b – 2] %= kMod; } } } ans[n][m] = 0; for (int a = 0; a <= m; a++) { for (int b = 0; a + b <= m; b++) { ans[n][m] += f[n][a][b]; ans[n][m] %= kMod; } } } } } int main() { Init(); int t,n,m; scanf("%d", &t); while (t–) { scanf("%d %d", &n, &m); printf("%lldn", ans[n][m]); } return 0; } [/cpp] 转换情况较多,代码和题解对照查看。 本代码提交AC,用时1698MS,内存9MB。]]>

hihoCoder 1068-RMQ-ST算法

hihoCoder 1068-RMQ-ST算法 #1068 : RMQ-ST算法 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi和小Ho在美国旅行了相当长的一段时间之后,终于准备要回国啦!而在回国之前,他们准备去超市采购一些当地特产——比如汉堡(大雾)之类的回国。 但等到了超市之后,小Hi和小Ho发现者超市拥有的商品种类实在太多了——他们实在看不过来了!于是小Hi决定向小Ho委派一个任务:假设整个货架上从左到右拜访了N种商品,并且依次标号为1到N,每次小Hi都给出一段区间[L, R],小Ho要做的是选出标号在这个区间内的所有商品重量最轻的一种,并且告诉小Hi这个商品的重量,于是他们就可以毫不费劲的买上一大堆东西了——多么可悲的选择困难症患者。 (虽然说每次给出的区间仍然要小Hi来进行决定——但是小Hi最终机智的选择了使用随机数生成这些区间!但是为什么小Hi不直接使用随机数生成购物清单呢?——问那么多做什么!) 提示一:二分法是宇宙至强之法!(真的么?) 提示二:线段树不也是二分法么? 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第1行为一个整数N,意义如前文所述。 每组测试数据的第2行为N个整数,分别描述每种商品的重量,其中第i个整数表示标号为i的商品的重量weight_i。 每组测试数据的第3行为一个整数Q,表示小Hi总共询问的次数。 每组测试数据的第N+4~N+Q+3行,每行分别描述一个询问,其中第N+i+3行为两个整数Li, Ri,表示小Hi询问的一个区间[Li, Ri]。 对于100%的数据,满足N<=10^6,Q<=10^6, 1<=Li<=Ri<=N,0<weight_i<=10^4。 输出 对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:标号在区间[Li, Ri]中的所有商品中重量最轻的商品的重量。 样例输入 10 7334 1556 8286 1640 2699 4807 8068 981 4120 2179 5 3 4 2 8 2 4 6 8 7 10 样例输出 1640 981 1556 981 981


给出n个数字,连续q次问[l,r]区间的最小值是多少。如果每询问一次都用遍历的方式求最小值,则总的时间复杂度为O(nq)。 本题给出了一个新的算法–RMQ-ST算法,它的时间复杂度为O(nlogn+q),如果q很大,比如接近n时,RMQ-ST算法就有优势了。 RMQ-ST算法的基本思想是先计算某个小区间a的最小值,当需要计算大区间b的最小值时,把它分成两个小区间a1,a2,因为小区间的最小值之前已经计算出来了,也就是a1,a2已知,则b=min{a1,a2}。这样每次询问[l,r]区间的最小值时,我们也只需要把[l,r]分成两个[l,t]和[t+1,r]区间,取这两个区间最小值的最小值,这样的时间复杂度是O(1)。 所以关键就在于怎么求所有小区间的最小值。 根据提示,为了简便,我们只求2的幂的长度区间的最小值,设数组int pre_calc[i][j]为区间[i,i+2^j-1]的最小值(从i开始,长度为2^j的区间最小值),因为长度为1的区间的最小值就是其本身,所以有pre_calc[i][0]=weight[i]。 因为pre_calc[i][j]的长度为2^j,所以我们正好可以将其分成2个长度为2^(j-1)的区间,也就是说pre_calc[i][j]=min{pre_calc[i][j-1],pre[i+2^(j-1)][j-1]}。 这样我们就找到了pre_calc的递推公式(实质是DP里的状态转换方程),那么j的取值范围是怎样的呢?因为长度为n的数组其最大子区间长度就是n,所以j的最大值就是log(n),下面只需一个二重循环就可求解。 当来了一个询问[l,r]时,我们也需要将其分成两个长度为2的幂的子区间a和b,假设这个幂为t,为了使两个子区间完全覆盖[l,r],两个子区间甚至可以重叠,所以a的右边缘需要大于等于b的左边缘,即l+2^t>=r-2^t+1,得到t>=log(r-l+1)-1,所以我们可以取t=log(r-l+1)。 完整代码如下: [cpp] #include<stdio.h> #include<math.h> #define MAX_N 1000010 int n,q; int weight[MAX_N]; int pre_calc[MAX_N][30]; //求a,b最小值 int get_min(int a,int b) { return a<b?a:b; } //初始化 void init_pre() { for(int j=1;j<=n;j++) pre_calc[j][0]=weight[j]; int m=floor(log((double)n)/log(2.0));//指数最大值 for(int i=1;i<=m;i++) { for(int j=n;j>0;j–) { pre_calc[j][i]=pre_calc[j][i-1]; if(j+(1<<(i-1))<=n)//如果另一半也存在的话。//1<<(i-1)其实就是2^(i-1) pre_calc[j][i]=get_min(pre_calc[j][i],pre_calc[j+(1<<(i-1))][i-1]); } } } int main() { //freopen("input.txt","r",stdin); scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&weight[i]); init_pre(); scanf("%d",&q); int l,r,t; while(q–) { scanf("%d%d",&l,&r); t=floor(log(double(r-l+1))/log(2.0));//找到能使[l,r]分为两半的指数 printf("%d\n",get_min(pre_calc[l][t],pre_calc[r-(1<<t)+1][t])); } return 0; } [/cpp] 代码中求pre_calc时需要注意二重循环的顺序不要搞反了,因为我们是先求得小区间的最小值才能求大区间的最小值,所以第一重循环应该是区间的长度,而不是区间的起点。 本代码提交AC,用时875MS,内存138MB。(用cin,cout超时。。。) 这篇文章介绍了用RMQ-ST算法求解区间最大值和最小值的方法。]]>

POJ 1163-The Triangle

POJ 1163-The Triangle The Triangle Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 38525 Accepted: 23126 Description Figure 1 shows a number triangle. Write a program that calculates the highest sum of numbers passed on a route that starts at the top and ends somewhere on the base. Each step can go either diagonally down to the left or diagonally down to the right. Input Your program is to read from standard input. The first line contains one integer N: the number of rows in the triangle. The following N lines describe the data of the triangle. The number of rows in the triangle is > 1 but <= 100. The numbers in the triangle, all integers, are between 0 and 99. Output Your program is to write to standard output. The highest sum is written as an integer. Sample Input 5 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 Sample Output 30 Source IOI 1994


DP水题一个,如果将等边三角形转换成Sample Input里的等腰直角三角形,设则某一点的路径和为sum[i][j],则因为只能从(i-1,j)和(i-1,j-1)这两个点到达(i,j),所以sum[i][j]=max{sum[i-1][j],sum[i-1][j-1]}+triangle[i][j]。如果为了节省空间,j可以从i downto 0循环,也就是DP表从右往左填,这样只需要一行空间。 不多说,简单题,直接上代码。 [cpp] #include<iostream> using namespace std; const int MAX_N=101; int triangle[MAX_N][MAX_N]; int sum[MAX_N]={0}; int DP(int n) { int rs=0; for(int i=0;i<n;i++) { for(int j=i;j>=0;j–)//从右往左填表,只需一行空间 { if(j>0) { sum[j]=(sum[j]>sum[j-1]?sum[j]:sum[j-1])+triangle[i][j]; } else if(j==0) { sum[j]+=triangle[i][j]; } if(sum[j]>rs)//记录最大值 rs=sum[j]; } } /*int rs=0; for(int i=0;i<n;i++) if(sum[i]>rs) rs=sum[i];*/ return rs; } int main() { //freopen("input.txt","r",stdin); int n; cin>>n; for(int i=0;i<n;i++) for(int j=0;j<=i;j++) cin>>triangle[i][j]; cout<<DP(n); return 0; } [/cpp] 本代码提交AC,用时0MS,内存260K。 ]]>