Week 12. 第260-282题

260. 只出现一次的数字 III

分析

同剑指offer73
先求所有数的异或和
因为不相同的数只有两个, 所以在异或和的第k位必定为1
然后根据第k位是否为1, 将原数组分成两部分, 就可以得到需要求的第1个数了
然后将sum ^ first就能得到第二个数了

code

class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        int sum = 0;
        for (auto& x : nums) sum ^= x;
        int k = 0;
        while (!(sum >> k & 1)) k ++;
        int first = 0;
        for (auto& x : nums) 
            if (x >> k & 1) first ^= x;
        return {first, sum ^ first};
    }
};

263. 丑数

分析

按照定义就行了

code

class Solution {
public:
    bool isUgly(int n) {
        if (n <= 0) return false;
        while (n % 2 == 0) n /= 2;
        while (n % 3 == 0) n /= 3;
        while (n % 5 == 0) n /= 5;
        return n == 1;
    }
};

264. 丑数 II

分析

其实这题考察的是 “多路归并”

将所有的丑数分成三大类,
第1类是所有包含因子2的丑数, S 2 S_2 S2
第2类是所有包含因子3的丑数, S 3 S_3 S3
第3类是所有包含因子5的丑数, S 5 S_5 S5
这3个集合并起来, S 2 ∪ S 3 ∪ S 5 ∪ 1 = S S_2 \cup S_3 \cup S_5 \cup 1 = S S2S3S51=S
S是整个丑数的集合

如果想从小到大将丑数集合 S S S列出来,
1, … 后面这个序列是这3个集合的并集, 所以接下来需要做的是将这3个集合归并一下

其实可以发现 S 2 = S ∗ 2 , S 3 = S ∗ 3 , S 5 = S ∗ 5 S_2 = S * 2, S_3 = S * 3, S_5 = S * 5 S2=S2,S3=S3,S5=S5
因此 S 2 , S 3 , S 5 S_2, S_3, S_5 S2,S3,S5可以由 S S S序列构造出来

所以3个指针, 可以同时指向S序列, 最开始都指向1

因为可能同时有多个数取到最小值, 因此需要把所有指到最小值的指针往后移动一位

在这里插入图片描述

code

class Solution {
public:
    int nthUglyNumber(int n) {
        vector<int> q(1, 1);
        for (int i = 0, j = 0, k = 0; q.size() < n; ){
            int t = min(q[i] * 2, min(q[j] * 3, q[k] * 5));
            q.push_back(t);
            if (q[i] * 2 == t) i ++ ;
            if (q[j] * 3 == t) j ++ ;
            if (q[k] * 5 == t) k ++ ;
        }
        return q.back();
    }
};

268. 丢失的数字

分析

脑经急转弯, 总和减去现有数的和

code

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int n = nums.size();
        int s = n * (n + 1) / 2;
        for (auto& x : nums) s -= x;
        return s;
    }
};

273. 整数转换英文表示

分析

因为英文是3位数, 3位数看的
所有最后面的3位是
xx hundred _ _, _ _表示几十, 几十的时候20~90正常, 1~19都有各自的表示

所有只需要写一个get(x)函数, 返回3位数

然后在最前面+单位, 比如billion, millon, thousand

在这里插入图片描述

零要特判下, 因为不会出现“zero million”

在这里插入图片描述

code

class Solution {
public:
    string num0_19[20] = {
        "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", 
        "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", 
        "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen",
        "Nineteen",
    };

    string num20_90[8] = {
        "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", 
        "Eighty", "Ninety",
    };

    string num1000[4] = {
        "Billion ", "Million ", "Thousand ", "",  // 因为最后3位没有任何单位, 所以接一个空字符就行了
    };

    string get(int x) {
        string res;
        if (x >= 100) {
            res += num0_19[x / 100] + " Hundred ";
            x %= 100;
        }

        if (x >= 20) {
            res += num20_90[x / 10 - 2] + " ";
            x %= 10;
            if (x) res += num0_19[x] + ' ';
        }else if (x) res += num0_19[x] + ' ';

        return res;
    }

    string numberToWords(int num) {
        if (!num) return "Zero";
        string res;
        for (int i = 1e9, j = 0; i >= 1; i /= 1000, j ++ ) // 最大是1e9, 单位是Billion开始计算
            if (num >= i){
                res += get(num / i) + num1000[j];
                num %= i;
            }
        
        res.pop_back(); // 多余的空格, pop_back()
        return res;
        
    }
};

274. H 指数

分析

首先为了方便, 将所有数排序

然后按照题意去枚举, 从n开始枚举h, 一共n个数, 所以h <= n
每次判断, 当前是否有h个数>= h
所以每次只需要判断前h个数是否都>= h, 因为所有数都已经从大到到小 排序, 所以只需要判断第h个数是否排好序

然后我们从大大小找到满足要求的数
这样的话, 时间复杂度是O(nlogn)

code

class Solution {
public:
    int hIndex(vector<int>& c) {
        sort(c.begin(), c.end(), greater<int>());

        for (int h = c.size(); h >= 1; h -- ) 
            if (c[h - 1] >= h) return h;
        
        return 0;
    }
};

275. H 指数 II

分析

如果用上一题的做法, 也是可以做的, 但是时间复杂度是O(n)的, 我们可以考虑下是否有更优的做法

考虑比O(n)更优的做法, 就只有考虑单调性了, 如果有单调性的话, 那么就可以考虑用二分去做

h取到这个位置的时候, 考虑h更靠右的时候, 因为当前的h是满足要求的最大的数, 所以比h靠右边的数(>h)不满足要求

那么看看左边, 如果h' < h

在这里插入图片描述

code

class Solution {
public:
    int hIndex(vector<int>& c) {
        int n = c.size(); 
        int l = 0, r = n;
        while (l < r) {
            int mid = l + r  + 1 >> 1;
            if (c[n - mid] >= mid) l = mid; // []内的坐标应该是从大到小数第mid个数
            // n - 1是第1个数
            // 所以第mid个数, 应该是 n - mid
            else r = mid - 1;
        }
        return r;
    }
};

278. 第一个错误的版本

分析

二分模板题

当前如果是错误版本, 那么第1个错误版本必定在当前左边, 所以r = mid
l = mid + 1

code

// The API isBadVersion is defined for you.
// bool isBadVersion(int version);

class Solution {
public:
    int firstBadVersion(int n) {
        int l = 1, r = n;
        while (l < r) {
            int mid = (long long)l + r >> 1;
            if (isBadVersion(mid)) r = mid;
            else l = mid + 1;
        }
        return r;
    }
};

279. 完全平方数

分析(完全背包)

n分成若干个完全平方数之和, 问最少分成多少项
如果不知道性质的话, 那么可以用完全背包 问题去做
n当成背包容量, 可以往里放1, 4, 9, 16, 25 , 每个数的数值当成体积, 价值当成1
然后题目就变成恰好装满背包的情况下,总价值最少是多少

完全背包的时间复杂度是O(nm), 该题目中物品个数的话是 n \sqrt n n ,
因为大于 n \sqrt n n 的数, 体积会超过 n n n

体积的话是 n n n, 所以用完全背包来做的话, 时间复杂度是 n n \sqrt n n n n

class Solution {
public:
    int numSquares(int n) {
        vector<int> dp(n + 1);
        for (int i = 1; i <= n; i ++ ) {
            dp[i] = i;
            for (int j = 1; i - j * j >= 0; j ++ )
                dp[i] = min(dp[i], dp[i - j * j] + 1);
        }

        return dp[n];

    }
};

分析(数学技巧)

  • 拉格朗日4平方和定理: 四平方和定理说明每个正整数均可表示为4个整数的平方和。
  • 勒让德三平方数之和: n能表示成3个整数的平方和 当且仅当 n ! = 4 a ( 8 b + 7 ) n != 4^{a}(8b + 7) n!=4a(8b+7)

所以可以试一下, 如果答案位1个数的平方和, 那么 n = ( n ) 2 n = (\sqrt n)^2 n=(n )2
1个数不行的话, 两个数, 枚举a, 考虑 b = n − a 2 b = \sqrt {n - a^2} b=na2 是不是整数就行了O(logn)次枚举
答案是 3的话, 只需要判断 n ! = 4 a ( 8 b + 7 ) n != 4^{a}(8b + 7) n!=4a(8b+7)两者的关系
以上都不行的话, 那么答案就是4

这样时间复杂度瓶颈是O( n \sqrt n n )

code

class Solution {
public:
    int check(int x){
        int r = sqrt(x);
        return r * r == x ;
    }

    int numSquares(int n) {
        if (check(n)) return 1; 

        for (int a = 1; a <= n / a; a ++ ) 
            if (check(n - a * a))
                return 2;

        while (n % 4 == 0) n /= 4;
        if (n % 8 != 7) return 3;
        return 4;
    }
};

282. 给表达式添加运算符

分析

每个空档一共4种填法, 总共 n − 1 n - 1 n1个空档, 所以方案数 O ( 4 n − 1 ) O(4^{n - 1}) O(4n1)

在这里插入图片描述
要做的, 就是把 4 n − 1 4^{n - 1} 4n1情况全部枚举出来, 枚举出来, 算下表达式的值是多少, 算完之后如果答案=目标值的话, 把答案存下来, 存答案还需要O(n)的时间, 所以整个时间复杂度是 O ( n 4 n ) O(n4^n) O(n4n)

可以维护一个代数结构 a + b ∗ _ a + b * \_ a+b_

在这里插入图片描述
当前等式是 a + b ∗ c a + b * c a+bc, c后面可以跟的运算符+, -, *
假设是+, a + b ∗ c + _ a + b * c + \_ a+bc+_, 那么可以整理下, 变成
( a + b ∗ c ) + 1 ∗ _ = a ′ + b ′ (a + b * c) + 1 * \_ \\ =a' + b' (a+bc)+1_=a+b

将(a + b * c)看成a’, 符号+后面的数看成1 * b’

在这里插入图片描述
所以, 可以发现不管下一个操作符是什么, 我们都可以用同样的代数结构去存

存这个结构就比较方便了, 我们在dfs的时候, 把前面的值存成这个结构就行了, 具体来讲存a, 和存b就可以了

把前面的值存成这个结构, 有什么样的好处呢, 起到压缩的作用; 不管前面的表达式有多么长, 我们都可以将表达式压缩成a, b

比方说, 前面表达式有100位, 压缩成了a, b, 所以代码就快了

code

typedef long long LL;

class Solution {
public:
    vector<string> ans;
    string path;

    vector<string> addOperators(string num, int target) {
        path.resize(100);
        dfs(num, 0, 0, 0, 1, target);
        return ans;
    }

    void dfs(string& num, int u, int len, LL a, LL b, LL target) {
        if (u == num.size()) {
            if (a == target) ans.push_back(path.substr(0, len - 1));
        } else {
            LL c = 0;
            for (int i = u; i < num.size(); i ++ ) {
                c = c * 10 + num[i] - '0';
                path[len ++ ] = num[i];

                // +
                path[len] = '+';
                dfs(num, i + 1, len + 1, a + b * c, 1, target);

                if (i + 1 < num.size()) {
                    // -
                    path[len] = '-';
                    dfs(num, i + 1, len + 1, a + b * c, -1, target);

                    // *
                    path[len] = '*';
                    dfs(num, i + 1, len + 1, a, b * c, target);
                }
                if (num[u] == '0') break;
            }
        }
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值