【刷题记录】April 2023 大三打基础,大四斗地主

153 阅读10分钟

权作记录,希望能在暑假的时候找个不打螺丝的实习

写写算法模板&考研题&阅读记录,给自己看的

目前的刷题量:

image.png

前几天笔试某度,第一题dp+单调栈,第二题dp看不懂,第三题OI分治树彻底击碎了鼠鼠的信心

感觉只有刷到四五百才会被当成个人

ALGORITHMS

N皇后镇楼

func solveNQueens(n int) (res [][]string) {
   diag1 := make([]bool, 2*n-1)
   diag2 := make([]bool, 2*n-1)
   col := make([]int, n)
   path := make([]bool, n)
   var dfs func(cur int)
   dfs = func(cur int) {
      if cur == n {
         board := make([]string, n)
         for i, c := range col {
            board[i] = strings.Repeat(".", c) + "Q" + strings.Repeat(".", n-1-c)
         }
         res = append(res, board)
         return
      }
      for c, on := range path {
         rc := cur - c + n - 1
         if !on && !diag1[cur+c] && !diag2[rc] {
            col[cur] = c
            path[c], diag1[cur+c], diag2[rc] = true, true, true
            dfs(cur + 1)
            path[c], diag1[cur+c], diag2[rc] = false, false, false
         }
      }
   }
   dfs(0)
   return
}

发现笔试中算法占比太大了..除了编程题之外在前面单选也考到了堆栈、哈希表、串等数据结构,所以打算本月份把数据结构和算法系统的过一遍,暂时就写写打卡题和复习题。主要过一遍408的数据结构,写写常用算法

链表

模板:

type ListNode struct {
   Val  int
   Next *ListNode
}

链表的模板没什么好说的,就是题目有关的链式操作会比较绕

一般比较难的链表题都要求使用两个以上的新指针保存数据然后在for里面绕,所以尤其需要理清楚当前情况下到底需要保存哪个结点的数据

目前碰到过的的题型操作大多是:

  1. 删除链表结点
  2. 在链表运算中判断是否添加新的结点(一般是加法运算要做进位,OF=(l1.Val+l2.Val)%10)
  3. 原链表分解成2个或多个链表(这种是最恶心的拆解,往往要思考半天才能理清楚逻辑)
  4. 双指针判断是否成环
  5. 深拷贝链表结点

也有一些逆天题在链表里面用单调栈,导致常规解法必须要引入保存索引的方法...

这样不就又变回数组了吗...

说的就是这逆天题1019. 链表中的下一个更大节点

栈和队列

字符串

查找

  • 顺序查找:没啥好说的,敢写他就敢把我挂了
  • 二分查找:时间复杂度O(log2n),数组必须有序
  • 缺失有序查找边界之类的关键词就要考虑想到二分了

个人不是很喜欢在二分查找中写开区间,所以一直都是写right = len-1; left <= right

普通的二分查找随便写

看之前写过的代码发现哪怕写反了条件之后的left=mid+1/right=mid-1也能找出结果..

对于middle计算,为防止OF最好使用middle = left + (right - left) >> 1

碰到的查找target边界倒是值得整个模板

34. 在排序数组中查找元素的第一个和最后一个位置

只需要再加一个bool值判断寻找的是左边界还是右边界即可

func BinarySearch(nums []int, target int, isLeft bool) int {
   left, right := 0, len(nums)-1
   res := -1
   for left <= right {
      middle := left + (right - left) >> 1
      if nums[middle] > target {
         right = middle - 1
      } else if nums[middle] < target {
         left = middle + 1
      } else {
         res = middle
         if isLeft {
            right = middle - 1
         } else {
            left = middle + 1
         }
      }
   }
   return res
}
#include<iostream>
#include<vector>
using namespace std;

int BinarySearch(vector<int>& nums, int target, bool isLeft) {
    int left = 0, right = nums.size() - 1;
    int res = -1;
    while ( left <= right ) {
        int middle = left + ( (right - left) >> 1);
        if ( nums[middle] > target ) {
            right = middle - 1;
        } else if ( nums[middle] < target ) {
            left = middle + 1;
        } else {
            res = middle;
            if ( isLeft ) {
                right = middle - 1;
            } else {
                left = middle + 1;
            }
        }
    } 
    return res;
}

int main () {
    vector<int> nums = {1, 2, 3, 3, 3, 4, 5, 7};
    cout << BinarySearch(nums, 3, true) << " " << BinarySearch(nums, 3, false) << endl;
    cout << BinarySearch(nums, 6, true) << endl;
    
    return 0;
}

不过在cpp里面有lower_bound直接用,在二维数组中用法如下:

    class Solution {
    public:
        bool searchMatrix(vector<vector<int>>& matrix, int target) {
            for ( const auto& row : matrix ) {
                auto it = lower_bound(row.begin(), row.end(), target);
                if ( it != row.end() && *it == target ) {
                    return true;
                }
            }
            return false;
        }
    };

区分开闭主要是因为每次要停下来思考一下right需不需要-1 or 直接等于

其实也完全可以写成全部都开的情况:(顺手写一下把左右边界拆成两个函数)

       // 右边界
        int BinarySearchRight(vector<int>& nums, int target) {
            int left = -1, right = nums.size();
            while (left + 1 < right) {
                int middle = left + ((right - left) >> 1);
                if(nums[middle] > target) {
                    right = middle;
                } else {
                    left = middle;
                }
            }
            return left;
        }

        // 左边界
        int BinarySearchLeft(vector<int>& nums, int target) {
            int left = -1, right = nums.size();
            while (left + 1 < right) {
                int middle = left + ((right - left) >> 1);
                if (nums[middle] < target) {
                    left = middle;
                } else {
                    right = middle;
                }
            }
            return right;
        }

题:

回溯

动态规划

dp这玩意感觉没法总结,没有板子能cv,不看佬题解自己思考递推公式的时候极其痛苦

Junior Dynamic Programming——动态规划初步·各种子序列问题

而且总结的话明显不如佬,不如不总结

300. 最长递增子序列不是很难,O(N2)解法两次遍历数组就可得到LIS

class Solution(object):
    def lengthOfLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        n = len(nums)
        if n == 0 :
            return 0
        dp = []
        for i in range(n):
            dp.append(1)
        for i in range(1, n):
            for j in range(0, i):
                if nums[j] < nums[i]:
                    dp[i] = max(dp[i], dp[j]+1)
        
        return max(dp)

找到一题类似的hard:354. 俄罗斯套娃信封问题

这题的类LIS O(N2)写法如下:

// time limit exceed
// N^2
func Max(i, j int) int {
	if i > j {
		return i
	}
	return j
}

func maxEnvelopes(envelopes [][]int) int {
	ans := 0
	n := len(envelopes)
	if n == 0 {
		return 0
	} else if n == 1 {
		return 1
	}
	sort.SliceStable(envelopes, func(i, j int) bool {
		if envelopes[i][0] != envelopes[j][0] {
			return envelopes[i][0] < envelopes[j][0]
		}
		return envelopes[i][1] > envelopes[j][1]
	})
	dp := make([]int, n)
	for i := range dp {
		dp[i] = 1
	}
	for i := 1; i < n; i++ {
		for j := 0; j < i; j++ {
			if envelopes[j][1] < envelopes[i][1] {
				dp[i] = Max(dp[i], dp[j]+1)
			}
		}
		ans = Max(ans, dp[i])
	}
	return ans
}

不过TLE了,没有力量,不然的话就这点难度还是不配当hard的...

image.png

只能去学二分优化版本的LIS哩QAQ

二分优化可以将时间复杂度优化为O(NlogN),本质上就是拿空间去换时间,理解起来也不是很难

声明一个stored数组存放已经严格排好序的子序列,之后每遍历到一个数时,在stored中查找是否存在比它大的数,如果存在,则将查找到的这个数换为遍历到的这个数

抽象成一句话大概就是:将生成的子数组变为严格递增『潜力』最大的子数组

func maxEnvelopes(envelopes [][]int) int {
	n := len(envelopes)
	if n == 0 {
		return 0
	} else if n == 1 {
		return 1
	}
	sort.SliceStable(envelopes, func(i, j int) bool {
		a := envelopes[i]
		b := envelopes[j]
		if a[0] != b[0] {
			return a[0] < b[0]
		}
		return a[1] < b[1]
	})
	// 声明一个stored切片存放已经成立的严格递增子序列
	stored := make([]int, 0)
	for i := 0; i < n; i++ {
		height := envelopes[i][1]
		if idx := sort.SearchInts(stored, height); idx < len(stored) {
			stored[idx] = height
		} else {
			stored = append(stored, height)
		}
	}
	return len(stored)
}

BE CAREFUL

二分优化后的LIS查找最终得到的结果并非是严格LIS,只是说明了数组中可以凑出来最有潜力的递增数组,实际上可能并不会得出二分优化后的答案

这题巧就巧在我们需要事先使用排序,避免了上述错误

LIS变体:

另一道典型dp

LCS 1143. 最长公共子序列

你一定要幸福啊

func longestCommonSubsequence(text1 string, text2 string) int {
   m := len(text1)
   n := len(text2)
   f := make([][]int, m+1)
   for i := 0; i <= m; i++ {
      f[i] = make([]int, n+1)
   }
   for i := 1; i <= m; i++ {
      for j := 1; j <= n; j++ {
         if text1[i-1] == text2[j-1] {
            f[i][j] = f[i-1][j-1] + 1
         } else {
            if f[i][j-1] > f[i-1][j] {
               f[i][j] = f[i][j-1]
            } else {
               f[i][j] = f[i-1][j]
            }
         }
      }
   }

   return f[m][n]
}

碰到的一些其他逆天题,基本上每次打开唯一的印象就是不会找递推公式:

双指针&滑动窗口

不把这俩分家的原因是目前的我认为一个滑动窗口依然也是依靠两个指针or下标来进行维护的

写到现在的感受到的区别差不多是:双指针重点关注下标,滑动窗口更关注窗口长度(窗口的维护)

排序

  • 冒泡:敢写就寄
  • 快排分治pivot划分,平均时间复杂度差不多可以看作O(nlog2n),平均空间复杂度是O(log2n)(借助了一个递归工作栈);快排并不稳定
  • 归并:将两个有序表归并为一个整的有序表。空间复杂度O(n), 时间复杂度O(nlog2n)

1508. 子数组和排序后的区间和

一道不错的题,纯暴力能A出来,最好再用非暴力方法优化一下

树&图

碰到树的基础题差不多就有这么几个点:

  1. 遍历:常规dfs/bfs,这一轮学习中也学会了queue遍历
  2. 验证:树是否符合某个性质or条件
  3. 构造:根据两个数组or其他数据结构来构造一棵树,或者根据给定条件把一棵树分解为其他数据结构
  4. 子树:最小生成树、最短路径

首先是树的遍历

比如下面几个板子题

碰到这些玩意的时候第一想法是dfs/bfs递归,不过貌似很容易栈溢出

抄的板子用的是保存*TreeNode的队列queue,每次将左右子结点保存在队列中

lc本篇中也有很多这样的常规题..感觉这板子缝缝补补还能用下一个十年

func levelOrder(root *TreeNode) (res []int) {
   if root == nil {
      return
   }
   queue := make([]*TreeNode, 0)
   queue = append(queue, root)
   for len(queue) != 0 {
      node := queue[0]
      queue = queue[1:]
      if node.Left != nil {
         queue = append(queue, node.Left)
      }
      if node.Right != nil {
         queue = append(queue, node.Right)
      }
      res = append(res, node.Val)
   }
   return
}

也用cpp写了几题,熟悉一下STL的queue使用

#include<iostream>
#include<vector>
#include<queue>
#include<algorithm>
using namespace std;

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};

class Solution {
public:
    vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
        vector<vector<int> > res;
        if (root == nullptr) {
            return res;
        }

        int cnt = 0;
        queue<TreeNode*> q;
        q.push(root);

        while( !q.empty() ) {
            auto length = q.size();
            vector<int> temp;
            cnt++;

            for ( int i = 0; i < length; i++ ) {
                auto node = q.front();
                q.pop();
                temp.push_back(node->val);
                if (node -> left != nullptr) {
                    q.push(node -> left);
                }
                if (node -> right != nullptr) {
                    q.push(node -> right);
                }
            }

            if ((cnt&1) == 0) {
                reverse(temp.begin(), temp.end());
            }
            res.push_back(temp);
        }
        return res;
    }
};

这题用cpp写的话有一个天坑, ==在cpp中的运算优先级是比&高,所以必须要在验证cnt时加个括号(cnt&1) == 0

树的验证

例如98. 验证二叉搜索树

根据性质有两种解法:

  1. 中序遍历构造value数组,确定是否满足二叉搜索树条件
  2. 递归验证每一个点是否满足条件

剑指 Offer 28. 对称的二叉树

这题是验证树是否满足镜像对称的性质

递归检查每一棵子树是否满足镜像对称即可

贪心

模拟

数学

MONTHLY SUMMARY

  • 草草过了一遍DB&ALGORITHMS
    • 速通了408数据结构
    • 大概水了剑指offer3-40题
    • 本月月底lc合计刷题量: image.png
  • 天胡局通了一关小骨,直接七童话+水小骨,很爽
    • 最后发现通的是难度0,通了才开魔镜,令人感叹
  • 阅读
    • 地狱变
    • 我本以为罗生门已经够邪门了,没想到有人比他还要邪门,这是谁的部将

其实这个月最后五六天已经基本懈怠等五一了,最后几天的daily都是(一小)半写(一大)半抄解决的

最后深刻的认知到自己是个fw的事实

这个月写的太狠,实际上很多都是临时记在脑子里的,之后的知识点都需要不断回滚

五月份的规划是

  • lc题目回滚,复习这个月做的题目,最好能一天折磨自己一道dp
  • os、net之类的专业课复健,预习ML和编译原理
  • 抄个后端小项目,让自己的简历不要太难看

祝大火五一快乐捏,不过我也不打算旅游,五一安心窝在大专漏水断电的宿舍里打游戏

👇 神,卡密

image.png