Week 8. 第131-140题

131. 分割回文串

分析

首先分割的方案可能有指数级别, 假设每一个字符都是相同的aaaaa, n个字母, 那么有n - 1个位置可以分割, 每个位置可以选择分/ 部分, 所以整个分割方案是2^n
发现分割方案是指数级别, 那么直接爆搜
爆搜的时候, 需要加一些优化, 因为爆搜的时候需要一段一段来搜

每次搜的时候, 会搜当前i位置开始的回文串, 比如搜到[i, j], 但是需要判断[i, j]区间是否是回文串,需要快速的判断原字符串的某个子段是否是回文串

如果说直接做, 可以线性扫描. 其实可以预处理下, f[i][j]数组, 表示原字符串的 [i, j]是否是回文串, 那这样判断的时候直接判断f数组就可以了O(1)

f[i][j] 计算是可以递推出来的, 递推的话很快, O(n^2)
在这里插入图片描述

递推的时候需要注意算f[i][j]的时候f[i + 1][j - 1]已经被计算出来了
所以应该从小到达枚举j

yxc评价本题

优化是一个比较常用的优化, 通过预处理, 来减少后面操作的时间

code

class Solution {
public:
    vector<vector<bool>> f;
    vector<vector<string>> ans;
    vector<string> path;
    vector<vector<string>> partition(string s) {
        int n = s.size();
        f = vector<vector<bool>> (n, vector<bool>(n));

        for (int j = 0; j < n; j ++ )
            for (int i = 0; i <= j; i ++ )
                if (i == j) f[i][j] = true;
                else if (s[i] == s[j]){
                    if (i + 1 > j - 1 || f[i + 1][j - 1]) f[i][j] = true;
                }

        dfs(s, 0);
        return ans;
    }

    void dfs(string& s, int u){
        if (u == s.size()){
            ans.push_back(path);
        }else {
            for (int i = u; i < s.size(); i ++ )
                if (f[u][i]){
                    path.push_back(s.substr(u, i - u + 1));
                    dfs(s, i + 1);
                    path.pop_back();
                }
            
        }
    }
};

132. 分割回文串 II

分析

跟上一题一样, 不过是返回最少的分割次数 <–>最少分成多少部分
分割的次数 = 分成的部分 - 1
通过上一题的分析, 可以发现分割的方案是指数级别的, 需要进行优化
在这里插入图片描述

同样g数组g[i][j]来定义[i,j]是否是回文串
g[i][j] = (s[i] == s[j]) && g[i + 1][j - 1], 先让j从小到大循环, 那么计算j的时候, g[i + 1][j - 1]一定会被算出来, 因为j - 1在j前面

yxc评价

预处理是dp很重要的思想, 如果不进行预处理, 在dp中check回文串, 要O(n), 总的时间复杂度会变成O(n^3)

code

class Solution {
public:
    int minCut(string s) {
        int n = s.size();
        s = ' ' + s;
        vector<vector<bool>> g(n + 1, vector<bool>(n + 1));

        for (int j = 1; j <= n; j ++ )
            for (int i = 1; i <= j; i ++ )
                if (i == j) g[i][j] = true;
                else if (s[i] == s[j]){
                    if (i + 1 > j - 1 || g[i + 1][j - 1]) g[i][j] = true;
                }

        vector<int> f(n + 1, 1e9);
        f[0] = 0; // 注意一定要初始化
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= i; j ++ )
                if (g[j][i])
                    f[i] = min(f[i], f[j - 1] + 1); // f[j - 1]
        return f[n] - 1;
            
    }
};

133. 克隆图

分析

要将所有的点复制一遍, 再将所有的边复制一遍
根据题意,

  1. 先将所有的点复制一遍
  2. 复制所有边 (注意边的双向的)
    克隆每一个点, 图的遍历有2种方式, bfs/dfs

遍历图与遍历树只有1点不同
遍历树的话, 没有环, 所以不会有重复的点
遍历图的话, 可能有重复的点, 判重就可以了

还需要定义映射关系, 原点->新点, 因为答案需要返回映射关系中的点
时间复杂度: 复制所有点O(n), 复制所有边O(m), 所以时间复杂度是线性的O(n + m)

在这里插入图片描述
图解释:
蓝色表示大哥, 红色表示小弟, 题意就是大哥之间有连线, 那么大哥的小弟与大哥的小弟之间也要有连线

code

/*
// Definition for a Node.
class Node {
public:
    int val;
    vector<Node*> neighbors;
    Node() {
        val = 0;
        neighbors = vector<Node*>();
    }
    Node(int _val) {
        val = _val;
        neighbors = vector<Node*>();
    }
    Node(int _val, vector<Node*> _neighbors) {
        val = _val;
        neighbors = _neighbors;
    }
};
*/

class Solution {
public:
    unordered_map<Node*, Node*> hash;
    Node* cloneGraph(Node* node) {
        if (!node) return NULL;
        dfs(node);

        for (auto [s, d] : hash){ // [s, d] 表示hash[s] = d
            for (auto ver : s->neighbors)
                d->neighbors.push_back(hash[ver]);// push ver对应的小弟
        }

        return hash[node];
    }

    void dfs(Node* node){
        hash[node] = new Node(node->val);

        for (auto ver : node->neighbors) // 搜当前点的邻点
            if (hash.count(ver) == 0) // 避免重复, 只有不在hash表中的邻点, 才继续搜
                dfs(ver);
    }
};

134. 加油站

分析

这题有一个针对这道题本身的做法, 非常的快, 也不需要开O(n)队列

本质上是贪心, 思路是先去枚举, 在对枚举做优化
枚举起点n次, 判断有没有出现负数的情况
比如从第i个起点开始, 首先判断下, 从第i个起点开始, 在第i个油站, 加的油量能否行驶到 i + 1油站, 如果可以的话, 先走到i + 1, 然后再在第i + 1的油站加油, 然后继续判断能否走到第 i + 2个油站, 以此类推, 看看从第i个加油站能否转一圈, 如果都可以满足, 说明它是成立的, 否则说明是不成立的

假如从第i个油站开始, 走到第j个油站失败了, 无法走到第 j + 1个油站了, 即从第j个油站剩余的油量left + g_j < cj, 那么就说明第i个起点是不合法的, 然后再枚举第i + 1个起点

如果直接这样暴力做的话, 起点n个, 每次最坏枚举n次, O(n^2)

然后我们可以发现这个过程可以优化
我们可以发现从第i个油站开始, 最多可以走到第j个站
考虑下[i + 1, j], 如果从[i + 1, j]任选一个起点, 那么是不是也一定无解呢?
在这里插入图片描述
随便枚举个点, 比如说k, 那么从第k个点开始走, 有没有可能转一圈呢?在这里插入图片描述

因为从第i个站开始走, 可以走到第k个站, 那么表示从i到k后还有剩余的油量, 即使有剩余的油量, 在第k站加油了, 也无法到达j + 1, 那么从第k个站开始走, 毫无油量, 不可能到j + 1, 更不可能走一圈了

因此我们在枚举的时候, 可以将中间的任何一个起点过滤掉

如果说发现从第i个站, 最多走到第j个站, 那么下一次枚举的时候直接从第j + 1个站开始枚举可以了

每个站最多枚举1次, 时间O(n), 额外空间O(1)
单调队列时间O(n), 空间O(n),但是单调队列可以解决其他很多问题

联动题

单调队列解法O(n) Acwing1088.旅行问题(算法提高课)
通用做法

code

i指标的写法, 先写成for (int i = 0; i < n; i ++ )
然后写到后面, 发现i 再往后跳j + 1的位置开始枚举,
for (int i = 0; i < n; i ++ ) 改成for (int i = 0; i < n;), 然后循环里填写i = i + j + 1

同理j指标的写法, 先写for (int j = 0; j < n; j ++ ) 然后发现外面要用到j, 修改成for (j = 0; j < n; j ++ ), 然后j填写到外面

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int n = gas.size();
        for (int i = 0, j; i < n; ){// 枚举起点
            int left = 0;
            for (j = 0; j < n; j ++ ){ // j表示从i开始走几个站, 0个站, ... , n个站
                int k = (i + j) % n;
                left += gas[k] - cost[k];
                if (left < 0) break;
            }
            if (j == n) return i;
            i = i + j + 1;
        }
        return -1;
    }
};

单调队列思想

单调队列的思想

环上任何一段, 都可以表示成一圈, 将一段复制一遍放到后面, 就是环了

比方从i开始走, 走长度是n的这一段, 走的过程中, 油量有没有可能是负数,
判断这一段有没有负数, 就是判断这一段最小值有没有负数, 本质上就是求长度是n的区间的最小值是否小于0, 相当于是滑动窗口求最小值的问题
显然可以用单调队列来优化

135. 分发糖果

分析

假设给每个小朋友分的糖果数量为 f i f_i fi
那么求的是ans = ∑ f i \sum f_i fi最小
考虑每个 f i f_i fi, 可以考虑从每个格子开始, 每次往两边走, 但是只能往较低的分值走, 那么 f i f_i fi >= 最长的步数
f i f_i fi >= 最长的步数的解释:

图中小朋友最多走3步, 那么就最少需要4颗糖
在这里插入图片描述

因此假设可以求得 s i : 最 多 走 几 步 s_i:最多走几步 si:
那么可以发现 f i ≥ s i f_i \geq s_i fisi
因此ans ≥ ∑ s i \geq \sum s_i si, 另外可以发现把 f i = s i f_i = s_i fi=si, 是满足要求, 相邻两个小朋友, 分值高的分到糖果比分值低的小朋友糖果多

反证法: 如果相邻两个小朋友分值为x, y, x > y, 但是 s x < s y s_x < s_y sx<sy, 那就不对了, 因为 s y s_y sy 表示从y往下走最多走几步, 那么x可以先走到y, 再从y开始走, 因此 s x > s y s_x > s_y sx>sy, 矛盾

因此 f i = s i f_i = s_i fi=si是一个合法的解, 然后我们证明了ans ≥ ∑ s i \geq \sum s_i si, 并且ans 可以取到 ∑ s i \sum s_i si, 因此 ∑ s i \sum s_i si就是最小值
因此这一题就是求下 s i s_i si就可以了, 转化为滑雪问题, 可以用记忆化搜索的方式来求
看下 s i s_i si两边, 两种情况去max
在这里插入图片描述

这样保证每个数只会算1次, O(n)

联动题

acwing 901.滑雪(该题的二维版本)

code

f[i]表示最小步数

class Solution {
public:
    vector<int> f; // 每个小朋友最多可以走的步数
    vector<int> w; // 小朋友的分值
    int n;
    int candy(vector<int >& ratings) {
        n = ratings.size();
        w = ratings;
        f.resize(n, -1); // 初始化为-1, 表示没有计算过

        int res = 0;
        for (int i = 0; i < n; i ++ ) res += dp(i);
        return res;
    }

    int dp(int x){
        if (f[x] != -1) return f[x];  
        f[x] = 1; // 小朋友至少1颗糖
        if (x && w[x] > w[x - 1]) f[x] = max(f[x], dp(x - 1) + 1); // 递归
        if (x + 1 < n && w[x + 1] < w[x]) f[x] = max(f[x], dp(x + 1) + 1); // 递归
        return f[x];
    }
};

136. 只出现一次的数字

分析

考察的是异或运算

  1. 交换律 a^b = b^a
  2. 结合律
  3. x ^ x = 0
    因为异或具有交换律, 相同数异或等于0, 因此将所有的数异或一下, 那么就只剩下出现次数为1的数了

扩展版本(思考题)

数组中有两个数出现1次, 其他数都出现2次, 求这两个数是啥
剑指offer 73. 数组中只出现一次的两个数字

code

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int res = 0;
        for (auto& x : nums) res ^= x;            
        return res;
    }
};

137. 只出现一次的数字 II

分析

DFA(状态自动机)

只需要统计32位的每一位, 搜有数的这一位1出现了多少次, 如果出现了3的倍数, 那么就说明只出现一次的那个数这一位是0; 否则(按照题目数据, 一定是3的倍数+ 1), 那个只出现1次的数的这1位一定是1

可以对每一位用状态机,模3余0的状态,模3余1的状态,模3余2的状态。来一个数如果这一位是0就留在这个状态,否则就0-1-2-0-1-2这样变化。

因为要表示3个状态,所以至少要有两个二进制位,让00表示0,01表示1,10表示2,只要设计一种运算让它满足上面的转换就

这种构造出来的运算是:
o n e = ( o n e ∧ x ) & ( ¬ t w o ) t w o = ( t w o ∧ x ) & ( ¬ o n e ) one = (one \wedge x) \& (¬two) \\ two = (two \wedge x) \& (¬one) one=(onex)&(two)two=(twox)&(one)
one和two都用32位的int,这样每个数的32位都能并行计算。
计算完成后, 寻找只出现1次的数, 就是每位并行计算后出现次数1位的状态, 就是one

在这里插入图片描述
该题状态机的含义: 当前这位(个位/ 十位/ 百位…/n位)1的个数%3的状态
每次来一个数, 都可以进行相应的转移

评论

lyt666 7个月前 回复
补充一下:状态01是由two、one共同决定,所以要找余1的位应该找two为0并且one为1的位,但循环结束two为0,所以one就是要找的结果

code

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int one = 0, two = 0;
        for (auto& x: nums){
            one = (one ^ x) & (~two);
            two = (two ^ x) & (~one);
        }
        return one;
    }
};

138. 复制带随机指针的链表

分析

可以像133.一样的如下步骤进行复制

  1. 复制点
  2. 复制边

yxc:上一题的做法就不讲了, 时间O(n), 空间O(n)

取巧

有一个取巧的方式, 省掉hash表
克隆完一个小弟(红色点), 然后删除原来大哥(蓝色点)之间的边, 将小弟(红色点)加到大哥之间
然后p的小弟p’的random指针的话, 可以根据p(大哥)的random(q)->next找到
在这里插入图片描述

新建一个虚拟头节点, 每次将当前点插入到虚拟头节点的后面, 然后修复原来的单链表(即:将大哥指向大哥), 以此类推(大哥指向大哥)
这样就抽出链表中的小弟了

在这里插入图片描述

code

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/

class Solution {
public:
    Node* copyRandomList(Node* head) {
        for (auto p = head; p; p = p->next->next){ // 复制小弟
            auto q = new Node(p->val);
            q->next = p->next;
            p->next = q;
        }
        for (auto p = head; p; p = p->next->next){// 复制random
            if (p->random)
            	// 小弟->random = 大哥->random->next 
                p->next->random = p->random->next;
        }
        // 分离两个链表
        auto dummy = new Node(-1), tail = dummy;
        for (auto p = head; p; p = p->next){
            auto q = p->next;
            tail = tail->next = q;
            p->next = q->next; // 恢复原状
        }
        return dummy->next;
    }
};

其中分离链表的操作也可以这样写

            tail = tail->next = p->next; // 找到小弟
            p->next = p->next->next; // 越过小弟

139. 单词拆分

分析

拆分的方式有很多种, 一个字符串拆分成很多个字母
n个字母, n - 1个空隙, 每个空隙可以选择要不要切分,那就 2 n − 1 2^{n - 1} 2n1种情况, 指数级别

直觉上和 131.分割回文串很像,
考虑能不能用dp去做

发现状态表示完全一样
f[i] 表示s[1~i]所有的合法划分方案
以最后一个单词为分割点
最后一个单词如果是1~i的话, 算第1类
2~i的话, 算第2类

i~i, 第i类
以第k类为例, [k, i]

可以分成两部分[1, k - 1], [k, i]
整个这类是否存在划分方案, 只需要看前面[1, k - 1]是否存在划分方案, 会发现前面这部分恰好就是f[k - 1]

所以[k, i]是否存在合法方案就只需要看下f[k - 1]就可以了, 但是前提是[k, i]在字典中出现过
在这里插入图片描述
时间复杂度:
状态计算是O(n)的, 每个状态在计算的时候, 一共有n次循环, 因为需要计算1~i, 2~i, … ,i~i, 所以n次
O(n^2)

注意

在这里插入图片描述

判断某一段是否在字典中出现是否出现过, 大部分人用的是hash表 + substr函数

unordered_set<string> dict(wordDict.begin(), wordDict.end());
dict.find(s.substr(j, i - j))

hash表的增删改查与字符串长度成正比, 这样写的话,判断的话是O(n)的, 这样的话总的时间复杂度是O(n^3)

如果想要做到O(n^2)的话, 需要额外的优化

优化

字符串O(1)的做法: trie树(平均O(1)), 字符串哈希, kmp
在这里插入图片描述

s = ’ ’ + s;的用意

因为f数组的边界是f[0], 需要将第0个位置空出来, 因此需要对原字符串做偏移

code

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        typedef unsigned long long ULL;
        const int P = 131;
        unordered_map<ULL> hash;
        for (auto& word : wordDict){
            ULL h = 0;
            for (auto c : word) h = h * 10 + c;
            hash.insert(h);
        }

        int n = s.size();
        vector<bool> f(n + 1);
        f[0] = true; // 表示原字符串一个都没有, 因此也是true
        s = ' ' + s; // 下标改成1开头
        for (int i = 0; i < n; i ++ )
            if (f[i]){ // 计算当前状态可以更新哪些后面的状态, 因为字符串哈希计算方式是p * 10 + 后面的字符的值, 算起来比较方便
                ULL h = 0;
                for (int j = i + 1; j <= n; j ++ ){
                    h = h * P + s[j];
               		if (hash.count(h)) f[j] = true;
               	}
            }
        return f[n];
    }
};

也可以从后往前枚举, 也方便计算hash值
f[i] 表示[i ~ n]是否存在合法的方案
那么[i ~ n]可以划分成[i ~i], [i ~ i + 1] , … [i ~ n]
以当前段[i, j]为例子
f[i, j]合法的话, 在+上f[j + 1]合法, 那么f[i]合法,

最后返回f[0]即可
yxc的代码
在这里插入图片描述
在这里插入图片描述

140. 单词拆分 II

分析

返回所有的划分方案
爆搜下划分方案
需要加一个优化
为了判断当前分支是否合法, 需要快速的判断后面一部分能否合法划分
所以要预处理f[i]: s[i ~ n]能否划分出来每一部分, 使得每一部分都在单词列表中出现过

预处理dp和上一题一模一样

因为爆搜是指数级别, 因此hash表不用像上一题优化, O(n^3)和指数级别差不了太多

for循环中
i >= 0 可以写成 ~i
i >= 1 可以写成 i

注意1:

因为dfs中要判断[i + 1, n]这段是否合法, 所以处理f的时候, f[i]只能定义成[i ~n]为合法区间, f的定义不能考虑[0 ~ i]区间的合法性

因此遍历的时候, 确定[i , n]的状态, 需要考虑[i, j] + f[j + 1]

注意2:

path 实参 只能为"", 中间不能有空格, 表示空字符串

code

class Solution {
public:
    vector<bool> f;
    vector<string> ans;
    unordered_set<string> hash;
    int n;
    vector<string> wordBreak(string s, vector<string>& wordDict) {
        n = s.size();
        f.resize(n + 1);
        for (auto word : wordDict) hash.insert(word);
        f[n] = true;
        for (int i = n - 1; i >= 0; i -- )
            for (int j = i; j < n; j ++ )
                if (hash.count(s.substr(i, j - i + 1)) && f[j + 1]) // 注意这里是[i, j] + f[j + 1]
                    f[i] = true;
        
        dfs(s, 0, "");

        return ans;
    }

    void dfs(string& s, int u, string path){
        if (u == n){
            path.pop_back(); // 取出path末尾的多余的1个空格
            ans.push_back(path);
        }else {
            for (int i = u; i < n; i ++ )
                if (hash.count(s.substr(u, i - u + 1)) && f[i + 1])
                    dfs(s, i + 1, path + s.substr(u, i - u + 1) + ' ');
        }
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
校园失物招领系统管理系统按照操作主体分为管理员和用户。管理员的功能包括字典管理、论坛管理、公告信息管理、失物招领管理、失物认领管理、寻物启示管理、寻物认领管理、用户管理、管理员管理。用户的功能等。该系统采用了Mysql数据库,Java语言,Spring Boot框架等技术进行编程实现。 校园失物招领系统管理系统可以提高校园失物招领系统信息管理问的解决效率,优化校园失物招领系统信息处理流程,保证校园失物招领系统信息数据的安全,它是一个非常可靠,非常安全的应用程序。 ,管理员权限操作的功能包括管理公告,管理校园失物招领系统信息,包括失物招领管理,培训管理,寻物启事管理,薪资管理等,可以管理公告。 失物招领管理界面,管理员在失物招领管理界面中可以对界面中显示,可以对失物招领信息的失物招领状态进行查看,可以添加新的失物招领信息等。寻物启事管理界面,管理员在寻物启事管理界面中查看寻物启事种类信息,寻物启事描述信息,新增寻物启事信息等。公告管理界面,管理员在公告管理界面中新增公告,可以删除公告。公告类型管理界面,管理员在公告类型管理界面查看公告的工作状态,可以对公告的数据进行导出,可以添加新公告的信息,可以编辑公告信息,删除公告信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值