LintCode 解题记录 17.11.11

前言

又是一年双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-2s[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);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值