前言
又是一年双11,今日你剁手了吗?被同学忽悠没忍住买了台显示器,1000左右,不得不说京东白条真是无底洞啊。。
Paint House
题目描述
有n个房子,每个房子都可以被涂红、蓝、绿三种颜色,假设第i个房子(i从0开始)涂第j个颜色的花费是cost[i, j], j = 0, 1, 2分别代表上述三种颜色。现在要求相邻的两间屋子不能涂相同的颜色,问你最小花费。
思路
跟House Robber很相似的题目,我们定义dp[i, j]是第i家房子涂第j个颜色时的最小花费。那么很容易写出如下递推公式:
dp[i, j] = min{dp[i-1, k] + cost[i][j], k = 0, 1, 2 && k != j}
边界条件dp[0, k] = cost[0, k], k = 0, 1, 2
代码
int minCost(vector<vector<int>> &costs) {
// write your code here
int n = costs.size();
vector<vector<int>> dp(n+1, vector<int>(3, INT_MAX));
dp[0][0] = dp[0][1] = dp[0][2] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 3; k++) {
if (k == j) continue;
dp[i][j] = min(dp[i][j], dp[i-1][k] + costs[i-1][j]);
}
}
}
return min(dp[n][0], min(dp[n][1], dp[n][2]));
}
Palindrome Partitioning II
题目描述
给定一个字符串s,将s切割成许多个子字符串,这些子字符串全是回文串。先让你求这个最小的切割次数s。
思路
首先这道题自己没有AC,原因在于把状态定义的不对,越想越复杂。
我们回到这道题的本身,定义状态dp[i]为前i个字符所需要的最小分割次数。那么有如下递推公式:
1.dp[i] = min{dp[j]+1, s[j:i-1] is a Palindrome, j=0,1,..,i-1}
//这个递推公式就是说,我当前前i个字符的最小分割次数,就等于前j个字符的分割次数+1,如果字符串s[j:i-1]是回文串的话。
//我觉得这道题的一个Key在于dp的边界初始化问题。
2.Initialization: dp[i] = i-1;
//也就是说,前i个字符最坏的情况就是一个字符一个字符的分割,注意这里的dp[0]要初始化为-1而不是0。考虑s直接是回文串的话,那么j=0,所以dp[i] = dp[0]+1 = 0,符合题意。
3.如何判断s[j:i-1]是否为回文串?其实这也是一个动态规划问题。
1)如果j == i-1,那么一个字符显然是回文串。
2)如果j == i-2且s[j] == s[i-1],那么这两个字符的字符串也是回文串。
3)如果s[j] == s[i-1]且 s[j+1:i-2]也是一个回文串,那么s[j:i-1]则也为回文串。
总结一下就是如下判断条件:
if (s[i-1] == s[j] && (j >= i-2 || isPalindrome[j:i-1])) then isPalindrome[j:i-1] is True
代码
根据上述思路,写出代码应该不是难事。
int minCut(string s) {
// write your code here
int n = s.size();
vector<int> dp(n+1, 0);
vector<vector<bool>> isPalindrome(n, vector<bool>(n, false));
for (int i = 0; i <= n; i++) {
dp[i] = i-1;
}
for (int i = 1; i <= n; i++) {
for (int j = i-1; j >= 0; j--) {
if (s[i-1] == s[j] && (j+2 >= i || isPalindrome[j+1][i-2] == true)) {
dp[i] = min(dp[i], dp[j]+1);
isPalindrome[j][i-1] = true;
}
}
}
return dp[n];
}
总结
上述的思路是我在看了别人的题解之后自己想的,因为别人状态的定义都是dp[i]表示[i, n]的最小分割次数,我就觉得很不舒服,因为很少看到这样从屁股后开始定义的,也可能是我才疏学浅了吧。看懂了别人的思路,然后自己弄了个从头定义的,最后也AC了。
Partition Equal Subset Sum
题目描述
给定一个非空数组,里面全是正整数,问你现在这两个集合是否可以划分为两个元素和一样的子集。
思路
背包问题。稍微把这道题转换一下就是给定一个数组,问你能否从里面选出若干个数,使其恰好装满一个固定容量的背包。这里的固定容量就是数组元素和的一半。至于“恰好装满”这种说法,只需要在初始化时将dp[0] = 0,dp[i] = INT_MIN(i=1,2,3…)即可。
代码
bool canPartition(vector<int> &nums) {
// write your code here
int sum = 0;
for (auto n : nums) {
sum += n;
}
if (sum % 2 == 1) return false;
int mid = sum / 2;
vector<int> dp(mid+1, INT_MIN);
dp[0] = 0;
for (int i = 0; i < nums.size(); i++) {
for (int j = mid; j >= nums[i]; j--) {
dp[j] = max(dp[j], dp[j-nums[i]] + nums[i]);
}
}
return dp[mid] == mid;
//这个地方我测试了一下,发现如果全部初始化为0,然后返回这个也能AC。也就是说,mid大的背包能装的最大容量如果为mid,也就说明装满了。但是此题的重点便在于对基础背包问题的理解与掌握。
}
Range Sum Query 2D - Immutable
题目描述
给定一个矩形,然后给两个坐标(row1, col1),(row2, col2),问你以这两个坐标为左上角和右下角形成的矩形的元素和是多少?
思路
可以预处理求出每一个点(x, y)和原点(0,0)组成的矩形的元素和,那么题意所要求的就可以直接用预处理的元素和进行加减得到。这里我们定义dp[i, j]为前i行和前j列的元素和,初始化为:
dp[i, j] = dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1] + matrix[i-1][j-1].
i = 1,2,..,m,j = 1,2,...,n
那么题目所要求的矩形和就等于:
dp[row2+1][col2+1] - dp[row2+1][col1] - dp[row1][col2+1] + dp[row1][col1]
代码
vector<vector<int>> global;
NumMatrix(vector<vector<int>> matrix) {
// do intialization if necessary
int m = matrix.size(), n = matrix[0].size();
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1] + matrix[i-1][j-1];
}
}
global = dp;
}
/*
* @param row1: An integer
* @param col1: An integer
* @param row2: An integer
* @param col2: An integer
* @return: An integer
*/
int sumRegion(int row1, int col1, int row2, int col2) {
// write your code here
int res = global[row2+1][col2+1];
res -= global[row1][col2+1];
res -= global[row2+1][col1];
res += global[row1][col1];
return res;
}
Nuts & Bolts Problem
题目描述
给定一个数组Nuts,一个数组Bolts,现在让你将这两个数组分别排序,使其对应位置上的Nuts和Bolts对应。给了一个Compare函数,它只能比较一个Nut和一个Bolt之间的大小关系,不能比较Nut和Nut之间、Bolt和Bolt之间的大小。
思路
我直接暴力O(n2)没想到LintCode直接给我过了:D,后来写此题解的时候上网搜了一下,原来才发现题目的本意并不是这样,考察的是Quick Selection。
思路是这样的,先从nuts随机选择一个作为pivot,以此为pivot对bolts进行分区,返回下标index,那么bolts数组中,index的左侧都是小于这个pivot,右侧都是大于这个pivot。而这个bolts[index]就等于之前随机选择的nuts。然后以此bolts[index]作为一个新的pivot,对nuts进行分区。同样也能将nuts划分为左侧小于该pivot,右侧大于该pivot。然后递归的进行左右排序。时间复杂度为O(nlogn)
代码
代码写起来仍磕磕绊绊,需要注意的细节地方特别多。
void sortNutsAndBolts(vector<string> &nuts, vector<string> &bolts, Comparator compare) {
// write your code here
if (nuts.size() != bolts.size() || nuts.size() == 0 || bolts.size() == 0)
return;
int n = nuts.size();
quickSelection(nuts, bolts, 0, n - 1, compare);
}
void quickSelection(vector<string> &nuts, vector<string> &bolts, int start, int end, Comparator compare) {
if (start >= end) return;
//first randomly select a nut and use this as a pivot to partition bolts.
int index = partition(bolts, start, end, nuts[start], compare);
//second use this bolts[index] as pivot and partition nuts.
partition(nuts, start, end, bolts[index], compare);
//do this iteratively untill start >= end.
quickSelection(nuts, bolts, start, index - 1, compare);
quickSelection(nuts, bolts, index + 1, end, compare);
}
int partition(vector<string> &items, int start, int end, string pivot, Comparator compare){
int i = start, j = end;
while (i < j) {
while (i < j && (compare.cmp(items[i], pivot) == -1 || compare.cmp(pivot, items[i]) == 1)) i++;
while (j > i && (compare.cmp(items[j], pivot) == 1 || compare.cmp(pivot, items[j]) == -1)) j--;
if (i < j) {
swap(items[i], items[j]);
}
}
return i;
}
注意
值得注意的地方就是cmp每次比较的是nuts和bolts,但是我一次只能单独对其中一个数组进行分区计算,所以比较的时候要两种都要比较。否则的话那么有可能产生左边的都大于pivot,右边的小于pivot这种情况。
Subarray Sum Closest
题目描述
给定一个整数数组,找出该数组的子数组中元素和最接近0的那个,并且返回这个子数组的开始下标与结束下标。
思路
依旧是自己没有做出来的一道题。
1.子区间和可以由前缀和数组相减快速得到
2.将前缀和数组按照从小到大排序,那么相邻元素之差显然是比较接近0的。
3.由于排序会打乱原有的下标关系,所以用一个struct node来绑定preSum和idx,重载结构体的小于号,利用sort进行排序。
代码
struct node {
int value, index;
node(int val, int idx):value(val), index(idx) {}
bool operator < (const node &o) {
return (value < o.value || (value == o.value && index < o.index));
}
};
vector<int> subarraySumClosest(vector<int> &nums) {
// write your code here
vector<int> preSum(nums.size(), 0);
vector<node> s;
s.push_back(node(0, -1));
int sum = 0;
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
s.push_back(node(sum, i));
}
sort(s.begin(), s.end());
int mincost = INT_MAX;
vector<int> res(2, 0);
for (int i = 0; i < s.size()-1; i++) {
int diff = s[i+1].value - s[i].value;
if (diff < mincost) {
mincost = diff;
res[0] = min(s[i].index, s[i+1].index)+1;
res[1] = max(s[i+1].index, s[i].index);
}
}
return res;
}
Divide Two Integers
题目描述
计算两个数相除,不能使用除操作与模操作。
思路
可以用位操作来进行。我们知道往左移动一位相当于乘以2,往右移动一位相当于除以2。所以对于除数,每次不断左移一位,知道左移一位的结果大于被除数,然后被除数减去该值。然后再做循环。
代码
int divide(int dividend, int divisor) {
// write your code here
bool positive = (dividend > 0 && divisor > 0) || (dividend < 0 && divisor < 0);
if (dividend == INT_MIN && divisor == -1) return INT_MAX;
long long m = abs((long long)dividend), n = abs((long long)divisor);
long long res = 0;
while (m >= n) {
long long t = n;
long long p = 1;
while (m >= (t << 1)) {
t = t << 1;
p = p << 1;
}
m -= t;
res += p;
}
return !positive ? -res : res;
}
Maximum Average Subarray
题目描述
给定一个正数、负数都有的子数组,同时指定一个长度k,让你返回一个长度大于或等于k的数组,使其元素的平均值最大。
思路
暴力法O(n2)解决,但是会超时。想了半天也没想到二分搜索是怎么做的,于是就放弃了。看了九章上的代码,自己想了一下,似乎明白了那么一点点。
二分搜索是有序的,首先,该平均值一定是在[min, max]之间,于是就得到了一个有序区间。以此作为初始边界条件,然后二分判断。
代码
double maxAverage(vector<int> &nums, int k) {
// write your code here
double l = INT_MAX, r = INT_MIN;
int n = nums.size();
for (int i = 0; i < n; i++) {
l = min(l, (double)nums[i]);
r = max(r, (double)nums[i]);
}
//首先求得数组的最大值与最小值,于是最终答案一定是在[min, max]之间,于是就得到了一个二分的区间。
vector<double> sum(n+1, 0);
double min_pre = 0;
while (r-l >= 1e-6) {
double mid = (r+l)/2.0;
double min_pre = 0; //min_pre代表某一[0:t]区间和,该区间和最小,初始化为0。
bool check = false;
for (int i = 1; i <= n; i++) {
sum[i] = sum[i-1] + (nums[i-1]-mid); //每一个元素都减去该mid值
if (i >= k && sum[i] - min_pre >= 0) {
//sum[i]-minpre就得到了[t+1, i]的区间和,该区间每一个元素都减去mid后求和仍大于等于0,说明该区间的平均值是大于等于mid的,跳出循环,更新l = mid;否则就向左搜索。
check = true;
break;
}
if (i >= k) {
//如果不满足上述的条件,则更新min_pre,就是去掉前面比较小的元素即改变t
min_pre = min(min_pre, sum[i-k+1]);
}
}
if (check) {
l = mid;
} else {
r = mid;
}
}
return l;
}
总结
这道题我光看九章上的代码也很难理解这种做法,有点刻意而为之的感觉。
Check Sum of Square Numbers
题目描述
判断一个数c能不能写成 c = a2 + b2.
思路
判断一个数x是不是整数, x == (int)x即可。
代码
bool checkSumOfSquareNumbers(int num) {
// write your code here
if (num < 0) return false;
if (num == 0) return true;
for (int i = 1; i <= sqrt(num); i++) {
int left = num - i*i;
if (sqrt(left) == (int)sqrt(left))
return true;
}
return false;
Insert Interval
题目描述
给定一些按起点排序的,不重叠的区间序列和一个新的区间,将该区间插入这些区间序列中使其仍然保持原来的顺序和不重叠性。
思路
由于原区间都有序,所以我只需要从左遍历该序列,并判断是否与新区间是否重叠即可。
如果pair.end < newInterval.start,没重叠,则将该pair存到vector里。遍历下一个pair,直到某一pair.end >= newInterval.start,说明发生重叠。此时产生的新区间的start就等于该pair.start和newInterval.start的较小值,新区间的end就等于该pair.end和newInterval.end的较大值。然后继续遍历下一个pair,直到pair.start > newInterval.end,将新区间存到vector里。最后再把剩余的区间遍历完。
代码
//只要理清过程,这道题就不是很难,主要考察思路是否顺畅。
vector<Interval> insert(vector<Interval> &intervals, Interval newInterval) {
// write your code here
if (intervals.size() == 0) return vector<Interval>{newInterval};
vector<Interval> res;
int i = 0, len = intervals.size();
while (i < len && intervals[i].end < newInterval.start) {
res.push_back(intervals[i]);
i++;
}
while (i < len && intervals[i].start <= newInterval.end) {
newInterval.start = min(intervals[i].start, newInterval.start);
newInterval.end = max(intervals[i].end, newInterval.end);
i++;
}
res.push_back(newInterval);
while (i < len) {
res.push_back(intervals[i++]);
}
return res;
}
Rotate List
题目描述
给定一个链表,要求返回其向右移动k次的新的链表。
思路
在原有链表上找到Rotate后的新链表的尾结点,然后该节点的下一个节点就是新链表的头结点,同时把原链表的头尾连起来,就组成了新链表。
而新尾结点实际上就是原有链表的倒数第(k+1)个节点。可以用两个指针找倒数第(k+1)节点。
代码
ListNode * rotateRight(ListNode * head, int k) {
// write your code here
if (head == NULL) return NULL;
ListNode *fast = head;
int len = 0;
while (fast) {
len++;
fast = fast->next;
}
k = k % len;
if (k == 0) return head;
fast = head;
for (int i = 0; i < k; i++) {
fast = fast->next;
}
ListNode *slow = head;
while (fast->next) {
fast = fast->next;
slow = slow->next;
}
fast->next = head;
head = slow->next;
slow->next = NULL;
return head;
}
总结
经典问题:找给定链表中的倒数第k个节点,用Two Pointers的方法。
A + B Problem
题目描述
计算两个整数的和,要求使用位运算。
思路1
位运算主要就是与&,或|,非!,异或^,以及移位运算。
我个人的思路就是把每个整数都当成32位Bit来看待,然后逐步判断每一位上的值,同时用一个bool变量判断是否有进位。
最后再用或运算添加到答案的某一位上去。
代码1
int aplusb(int a, int b) {
// write your code here
bool carry = false;
int res = 0;
for (int i = 0; i < 32; i++) {
int ta = (a >> i) & 1;
int tb = (b >> i) & 1;
int tc = 0;
if (ta == 1 && tb == 1) {
tc = carry ? 1 : 0;
carry = true;
} else if (ta == 1 || tb == 1) {
tc = carry ? 0 : 1;
} else {
tc = carry ? 1 : 0;
carry = false;
}
res |= (tc << i);
}
return res;
}
思路2
看了九章的Java代码(C++和Python版的写的也太鸡儿草率了),发现:异或其实就是两个数没有进位的加法,那么何时产生进位呢?就是两个位置都为1的情况,即a & b,所以(a & b)<<1就是进位的结果。接下来就是计算 a^b + (a & b) << 1。这显然又回到了最初的问题,(其实有递归的形式)。令a = a ^ b, b = (a & b) << 1,不断循环操作就能得到a+b的结果。循环终止的条件就是全部的进位为0,即b==0.
代码2
int aplusb(int a, int b) {
// write your code here
while (b) {
int _a = a ^ b;
int _b = (a & b) << 1;
a = _a;
b = _b;
}
return a;
}
//递归形式
int aplusb(int a, int b) {
// write your code here
if (b == 0) return a;
return aplusb(a^b, (a&b)<<1);
}