cpuwdl 发布的文章

一些LeetCode中等题做法(二)

记录一些LeetCode中等题解法,少量分析。

116. Populating Next Right Pointers in Each Node I

题目:为二叉树实现记录next节点(层次遍历的下一个节点)。perfect binary tree (ie, all leaves are at the same level, and every parent has two children).

做法:外层循环遍历每一层的最左节点,除了最后一层,对于每一个最左节点,由于不是最后一层,所以肯定有左节点和右节点。首先将该节点P的左节点的next指向右节点,如果每层处理结束后,该层下面一层所有的next节点都维护完毕(在初始遍历根节点时,只需要根节点的左节点的next指向右节点就维护完毕)。对于当前层的每一个节点P,如果它有nextP->right->next=P->next->left,这时此节点P的左右孩子的next都已经记录,然后由于此层的next已经记录,需要将当前节点P移动至next节点,继续进行即可。最后当前层遍历结束,就维护了下一层的next节点。

117. Populating Next Right Pointers in Each Node II

题目:为二叉树实现记录next节点(层次遍历的下一个节点),允许节点的左右节点不完整。

做法:外层循环需要从每一层的第一个节点开始,首先假设在遍历当前层的时候,当前层的next已经维护好,在遍历过程中,维护好下一层的next节点。每层的第一个节点需要在维护下一层next节点时,对于第一个非空的左节点或右节点进行记录。由于该节点最后需要返回,作为下一层的开始,但是该节点的next需要不断地链接在最后一个next后面,所以要用两个指针,一个记录链表头,一个记录链表尾,不断在链表尾插入新来的next节点。

173. Binary Search Tree Iterator

题目:O(1)时间,O(树高)空间,对BST设计迭代器next函数和hasNext函数。

做法:维护一个栈,栈顶是当前的最小值,如果栈为空,则没有next,初始情况的最小值是从根节点开始,找到最左节点,栈中记录了从根节点到该节点的路径。每次求next,需要弹出栈顶的元素作为返回值,然后需要维护栈,要用临时变量指向弹出的元素,进行一些操作。栈顶元素是没有左节点的(或者所有左节点都已经弹出),如果该元素没有右节点,则弹出该元素后,栈顶的左子树已经全部弹出,栈顶就是当前的最小值。如果该元素有右节点,则首先需要将右节点入栈,然后将这个右节点的所有左节点依次入栈,最后栈顶是当前的最小值。

192. Word Frequency

题目:使用Bash统计文本词频,按词频大小逆序分行输出词语和词频。

做法:

cat words.txt | tr -s ' ' '\n' | sort | uniq -c | sort -nr | awk '{ print $2, $1 }'

| 管道命令,将前一个操作的输出用于后一个操作的输入。

cat words.txt 显示文件内容

tr -s ' ' '\n' tr命令是字符替换命令,tr SET1 SET2表示在SET1中的字符被替换为SET2对应位置上的字符,-s表示连续的SET1中的字符缩减为1个。于是该命令表示将连续空格替换为’\n’

sort 按行排序,sort -r 倒序,sort -nr 按数字倒序。

uniq 去除相邻重复行,uniq -c 去除相邻重复行并且计数,数量在前,行内容在后。

awk '{ print $2, $1 }' 对每行的元素,先输出第二个,再输出第一个。常用格式 awk ”模式awk {操作}awk “模式” {操作},输入文件每行作为一个元素,如果指定模式或操作,需要对匹配模式的行进行操作,默认操作是输出,默认样式是匹配所有字符串。$1对应分隔符划分的第一个元素,$2是第二个,分隔符默认是空格和制表符。如果没有用管道,末尾要加上操作哪个文件。

194. Transpose File

题目:使用Bash对一个多行由空格隔开的文本矩阵进行转置。

做法:

awk '
{
    for (i = 1; i <= NF; i++) {
        if(NR == 1) {
            s[i] = $i;
        } else {
            s[i] = s[i] " " $i;
        }
    }
}
END {
    for (i = 1; s[i] != ""; i++) {
        print s[i];
    }
}' file.txt

awk '{操作}END{后操作}' 文件名,先执行前面的操作,这里的操作是以行为单位,$1$2等对应的是行内的第一第二个元素,然后执行END内操作。NF是每行的元素个数,NR是当前累计的行数。所以该代码对第一行的每个元素,建立对应位置的数组,对后面的行的每个元素,找到相应的数组,加个空格再记录当前元素,得到的数组顺序输出就是原内容的转置。

142. Linked List Cycle II

题目:找到链表中的环入口,没环输出NULL,不能修改链表,进阶:O(1)空间。

做法:用map保存节点可以在O(n)空间完成,可以找到O(1)空间的解法。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode *slow = head, *fast = head;
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
            if (slow == fast) break;
        }
        if (!fast || !fast->next) return NULL;
        slow = head;
        while (slow != fast) {
            slow = slow->next;
            fast = fast->next;
        }
        return slow;
    }
};

x为链表入口到循环入口的距离,y为循环的长度,i为使得slowfast相遇的在0y-1范围内的值。slow走过的长度为x+ay+ifast走过的长度为x+by+i,有2(x+ay+i)=x+by+i,可得x+i=(b-2a)y,将x转换为模y的形式可以发现必然有一个a=0的解,则时间复杂度是O(N)。现在对比从链表头移动x次和fast移动x次,x+by+i+x=x+by+(i+x)=x+by+(b-2a)y=x+2(b-a)y,和链表头移动x次是相等的,并且位于循环的入口。

287. Find the Duplicate Number

题目:O(1)空间,小于O(N2)时间,不修改数组,在长度为N+1的只包含1N的数组中,寻找唯一的出现次数大于1的数,其他数只出现一次。

做法:我想到的办法是O(NlogN)的,二分查找leftright中间值mid,统计数组中位于区间[left,mid](mid,right]的数的数量LR,如果L<=Rleft=mid+1,否则right=mid。这题可以找到一个更优的O(N)的解法。

class Solution {
    public int findDuplicate(int[] nums) {
        // Find the intersection point of the two runners.
        int tortoise = nums[0];
        int hare = nums[0];
        do {
            tortoise = nums[tortoise];
            hare = nums[nums[hare]];
        } while (tortoise != hare);
        // Find the "entrance" to the cycle.
        int ptr1 = nums[0];
        int ptr2 = tortoise;
        while (ptr1 != ptr2) {
            ptr1 = nums[ptr1];
            ptr2 = nums[ptr2];
        }
        return ptr1;
    }
}

这题的做法和142. Linked List Cycle II的做法很像,用一个递增1的和一个递增2的指针找到它们相等的位置,然后把递增1的指针重置,然后和递增2的指针位置一起递增1,直到相等。这题考虑数组的下标与数组的值对,(i,v)表示节点i到节点v有一条边,下标范围是[0,N],值范围是[1,N],需要证明从0出发的链表一定有环。假设没有环,从0出发前进N+1次,每次得到的都得是不同的节点,而可选的边的末尾只有N个可能的值,矛盾,所以从0出发的链表一定有环。使用142题的方法可以找到环的入口,环的入口意味着有两个下标都能得到同一个值,也就是找到了数组中的重复值。

162. Find Peak Element

题目:对数时间,给定相邻数组值不等的数组,寻找任意一个大于左右邻居的数,数组边缘默认大于越界邻居。

做法:整体思路是二分不断获得更小区间,在递归或使用头尾指针二分时,对于一段位于数组内部的区间(不包含原数组头和尾),如果该区间中的元素构成递增数列或递减数列,则无法在该区间内找到所需的数,所以设计的方法要避免这种情况。另外,如果一段区间的头和尾都大于越界邻居时,就算该区间内部元素构成递增或递减数列,总可以在头或尾找到一个符合条件的数。首先考察数组的头尾,由于位于头尾的数默认是大于越界邻居的,于是在初始时限定了不是普通的递增或递减数列,并且满足头部递增,尾部递减。考察区间中间的元素,如果它大于该元素左侧一格的元素,则取右半边的区域,该区域满足头部递增(大于越界邻居),尾部递减,如果它小于左侧一格的元素,则取左半边区域,同样满足头部递增尾部递减,当区间只剩一个数时,它大于左侧和右侧越界邻居,则可以输出对应索引。

264. Ugly Nubmer II

题目:丑数是1和因子只含235的正整数,求第N个丑数。

做法:根据已有的丑数数组,求下一个丑数,需要把所有的丑数都乘以235,然后找到其中最小的数,这个数就是下一个丑数,依次计算就可以得到第N个丑数。但是如果所有丑数都要乘以235,效率太低,考虑当前已有丑数的最大值M,已有丑数中乘以2仍然小于等于M的不需要再考虑了,只有乘以2后大于M的第一个数是需要考虑的,35也同样。所以需要维护的是三个下标abc,分别表示乘以235大于当前丑数数组最大值的三个数的下标,当前丑数数组初始值为一个数字1。每次遍历需要考虑下标为abc的数乘以235的最小值,取为下一个丑数,如果这个新的丑数等于下标为a的数乘以2,则a需要递增,对bc也同样进行考察是否需要递增。

313. Super Ugly Number

题目:给定一个质数数组,超级丑数是1和能因子只在该数组中的数的正整数,求第N个超级丑数。

做法:与因子为235的丑数类似,为给定质数数组中的每个质数设置一个下标变量,遍历时考虑多个值。

307. Range Sum Query - Mutable

题目:整数数组单点修改,求指定区间和,线段树(树状数组)。

做法:明显的办法是O(1)修改,O(N)求和,可以找到一个稍微好一点的办法,是将原数组划分为sqrt(N-1)+1个sqrt(N)大小的区间,预处理O(N),更新时对受影响的一个区间更新O(1),区间和对包含的完整区间直接求和,不完整区间单个计算O(sqrt(N))

更好的办法是用线段树,将原数组划分为长度为NN/221的区间,每个区间记录区间和,修改时对根到叶节点的路径上的区间进行修改,区间求和需要将指定区间划分为已经记录和的多个区间求和。

线段树使用数组记录树形结构,每一个数组元素根据下标可以算出它在树形结构中的位置,数组元素表示对应区间内所有数的和。这里讨论的是递归形式的线段树,对于数组长度为N的数组,树的划分是固定的,接下来计算空间复杂度。假设给定的数组元素个数为2n,对应的线段树是一个完整的二叉树,每层节点数量分别为20212n,每层节点对应的区间长度是2n 2n-120,树总共有n+1层。对应树形结构,根节点下标为0,它的孩子下标为2*0+12*0+2(还有另一种方法是根节点是1,孩子下标2*12*1+1,浪费一个空间看着顺眼点,并且在计算的时候用位运算可以加速),下标为i的节点的孩子坐标为2*i+1,和2*i+2,总共能延伸n次,每次延伸获得的两个节点下标最大是2*i+2,令f(0)=0f(n)=f(n-1)*2+2,可得f(n)+2=2*(f(n-1)+2),则f(n)=2n+1-2,每层节点编号的最大值是022n+1-2。对于一个2n长度的数组,需要使用的空间是2n+1-1(要加上0),并且当数组长度在(2n-1,2n]内时,从根节点到叶节点在最坏情况下需要探索n次(探索n-1次最多获得2n-1个数),所以需要的空间倍数小于4(2n+1-1)/( 2n-1+1)),当考虑数组长度是N时,对应的空间复杂度是4N

现在稍微考虑一下极限空间是多少(如果是代码的话,建树的代码记录叶节点的线段树下标最大值就可以得到),在划分各个节点时,左右子树的节点数量差最大是1,所以左右子树高度差最大是1,需要考虑的是对应树形结构最后一层中最大的下标编号。原数组元素数量与所需空间的部分对应关系如下所示,2n-1+1->2n+12n-1+2->2n+2n-1+12n-1+2n-1->2n+2n-1,根据数组建树,然后根据递推关系找到最大的线段树节点下标加1就是所需的空间。原数组大小从2n-1开始,增加1,空间是2n+1,增加2,空间是2n+2n-1+1,增加4,空间再加2n-2,所以增加2m时,空间在上一次增加的基础上增加2n-m。需要注意的地方是,原数组大小在增加的过程中,对线段树的最下层的影响是,左边加一个,右边再加一个,交替进行,只有当增加到一定程度,才会影响最后的消耗空间,还有线段树的节点是占两个位置,在考虑的时候要取第二个位置,并且在计算的下标最大值的基础上加1才是消耗的空间。大概就是[2n-1+2m, 2n-1+2m+1)对应2n+2n-1+…+2n-m+1,也就是2n+1-2n-m+1,在边界m=n-1时,对应2n+1-2+1,也是成立的

按照这个式子,可以得到一个O(loglogN)时间,O(logN)空间的计算方法,大小为10的数组,最多可以计算原数组大小为256的情况。

int z[10] = {1,2,4,8,16,32,64,128,256,512};
int get(int a){
         int n=0, m=-1;
         n = lower_bound(z,z+10,a)-z;
         if(n>0){
                   int b = a-z[n-1];
                   m = upper_bound(z,z+10,b)-z-1;
         }
         return z[n+1] - z[n-m] + 1;
}

线段树中叶节点的数量与原数组的大小相等,在单点更新时,从根节点到对应叶节点的路径上的所有区间都要更新。从线段树的根出发,根据要更新的点的索引和当前区间的中点的关系,探索相应的节点,递归栈中有从根节点到相应叶节点的所有节点(线段树数组中对应的位置),先修改叶节点,然后逐个修改递归栈中的节点。

在区间查询时,从线段树的根节点出发,根节点表示整个原数组,每次将当前节点分成两份,对应线段树中的左右节点。在划分时,如果当前节点与要查询的区间没有交集,则返回0,如果有交集,只有在当前节点完全被要查询的区间包围时,返回节点对应的区间和。递归的过程会尝试探索线段树所有的节点,在超出范围的地方剪枝返回0,在恰好包含的地方剪枝返回区间和。为了便于理解,可以将线段树区间[begin,end]划分点S与要查找的区间[L,R]的关系分为两种情况,如果S位于LR两端,新的两个线段树节点有一个超出范围被剪枝,线段树查询的范围缩小了一半,如果S位于LR中间,在下一轮操作时,相当于在两个更小的线段树区间[begin,S][S+1,end]内,查找两个更小的查询区间[L,S][S+1,R],这样就不是在包含的时候输出,而是在精确对应的时候输出。证明时间复杂度是O(logN)较为复杂,可以参考https://www.cnblogs.com/AC-King/p/7789013.html。线段树经过修改可以实现O(logN)区间修改,之后如果有机会理解再做介绍,另外,对于这类问题,其中一部分可以使用树状数组记录前缀和,代码相对好写。

337. House Robber III

题目:正整数二叉树型结构,不能取相邻节点,取若干点求和,求所能取到的最大值。

做法:树形动态规划的基础题,从根节点开始,遍历整棵树,根据状态转移方程,由节点的子节点的信息得到父节点的信息。假设每个节点只记录一个信息,这个信息可以是选取该节点的子树的最大和,也可以是不选取该节点的子树的最大和,也可以是前两者的最大和,可以发现这三种信息都不足以由子节点推出父节点的信息。假设记录两个信息,不选取该节点的子树的最大和Sno是必须有的,另一个信息考虑以该节点为根的子树最大和SmaxSmax[root] = Sno[left] + Sno[right] + Val[root]Sno[root] = Smax[left] + Smax[right]Smax[root] = max(Smax[root], Sno[root])。使用深度优先搜索可以遍历所有节点,得到根节点的Smax,就是所能取到的最大值。

215. Kth Lagest Element in an Array

题目:求整数数组中第K大的数。

做法:维护一个大小为K的最小堆,保证堆里面是遍历过程中最大的K个数,遍历整数数组,如果堆的元素数量小于K,则直接插入,否则对比当前数与堆顶的大小,如果当前数大于堆顶,说明堆顶是当前已知的第K+1大的数,需要弹出,然后插入当前数,最后堆顶就是第K大的数。

347. Top K Frequent Elements

题目:找到整数数组中出现频率最高的K个数。

做法:与求第K大的数类似,需要维护一个大小为K的最小堆。建立一个结构或者类,包含数值与出现次数,遍历记录各个数值的出现次数。自定义compare函数,默认priority_queue是最大堆,定义a.m_count > b.m_count可以得到最小堆。

378. Kth Smallest Element in a Sorted Matrix

题目:每行每列都是递增的整数矩阵,找到其中第K小的数。

做法:比较明显的方法是用一个数组记录当前每一行已经探索了几个数,每次探索一个最小值,直到找到第K小的数。可以找到一个更好的方法,回顾一下行列递增矩阵的查找,初始选取整个矩阵,考虑要找的数N与当前矩阵右上角的数M的大小关系,如果N>M则第一行被排除了,这意味着去掉的部分有当前矩阵列数的数比要找的数小,如果N<M则倒数第一列被排除了,去掉的部分有当前矩阵行数的数比要找的数大。根据这个方法,选定一个数,在N>=MN<M时都进行行列的删除,同时记录总共有多少个数小于选定的数。用二分每次在二维数组中找一个数,得到有几个数小于该数,对比与K的关系,修改要查找的数。

一些LeetCode中等题做法(一)

记录一些LeetCode中等题解法。

15. 3Sum

题目:给定一个整数数组,求在数组中取三个数相加和为0的所有非重复元组。

做法:遍历数组建立map,使得查找某个值是否在数组中的操作变为O(1),双重循环遍历数对,计算0减去这两个数得到M, 在map中查找M,如果找到则得到一个三元组,在插入结果数组之前,排序后用map去重。这个方法比较明显但是相对较慢,可以找到一个更好的解法。首先对数组排序,依次遍历各个数,相当于在剩下的数中选取两个指定和的数,用两个指针指向头和尾,根据两个指针指向的数之和与目标和的大小关系,移动指针寻找和为指定值的两个数,如果找到了就记录一下,然后把头尾指针移到和当前值不等的数上,在头尾重合的时候终止。遍历完第一个数后,要跳过和第一个数相等的数,这样最后就没有重复的三元组。

16. 3Sum Closest

题目:给定一个整数数组和目标整数,求在数组中取三个数相加离目标整数最近的整数。

做法:对数组排序,遍历数组中的除了后两个数以外的数,在剩下的数中记录头尾指针,如果对应的三个数的和比已经记录的最接近目标值的数还要接近目标值,则更新已经记录的值,根据和与目标值的大小关系移动头尾指针,大于目标值则尾指针减一,小于目标值则头指针加一。

18. 4Sum

题目:给定一个整数数组和目标整数,求所有和为目标整数的非重复四元组。

做法:对数组排序,双重循环遍历数对,根据情况移动保证前两个数组成的元组不重复,如果加上剩下的数中最大的两个依然小于目标值,或者加上最小的两个都大于目标值则跳过,接着就按照正常的两数和用头尾指针移动并去重,最后得到的是不重复的四元组。

454. 4Sum II

题目:4个等长整数数组,在4个数组中各取一个数求和为0,求取法数量。

做法:用map记录前两个数组中数对的和,key为和,value为数量,遍历后两个数组中的数对c和d,在前面记录的map中寻找0-c-d,如果找到了就增加对应数量的取法。

39. Combination Sum I

题目:给定一个整数集合和目标整数,集合中的数可以多次使用,求所有和为目标整数的非重复元组。

做法:对数组排序,递归调用,参数包含原整数数组,当前已经使用的数构成的数组,与目标整数的差值,遍历数组的起始位置。如果与目标整数的差值为0,将当前数组加入结果数组,否则遍历原整数数组,如果当前数小于等于当前目标值,将当前数加入已用数组,递归调用,然后把当前数弹出。

40. Combination Sum II

题目:给定一个整数数组和目标整数,数组中的数只能使用一次,求所有和为目标整数的非重复元组。

做法:与前一道题的差别在于当前遍历的数不能和上一个数相同,并且递归时遍历数组的起始位置要递增1。

74. Search a 2D Matrix

题目:每行递增排序的二维数组,并且每一行的开头大于前一行的末尾,查找是否包含目标值。

做法:相当于一个排序数组的二分查找,需要对一维下标和二维下标转化。

240. Search a 2D Matrix II

题目:每行每列都是递增排序的二维数组,查找是否包含目标值。

做法:如果取中点,划分出来的两部分不能形成子结构。初始为整个矩形区域,需要每次选取剩余矩形的右上角,如果右上角小于目标值,则去掉第一行,如果右上角大于目标值,则去掉最右边的列,是一个O(M+N)的解法。

137. Single Number II

题目:线性时间,O(1)空间,求每一个元素只可能出现一次或三次的整数数组中出现一次的数。

做法:这里的线性时间需要用到整数32位的限制,遍历一次数组,计算最末位的1的和,该和模3的结果是只出现一次的数的最末位的值,然后一次遍历32位,然后拼起来就得到结果。

260. Single Number III

题目:线性时间,O(1)空间,给定整数数组中有两个数只出现一次,其他的数都是出现两次,求只出现一次的两个数。

做法:这里需要用到异或的一个特性,如果一个数组只有一个数出现一次,其他数都是两次,对数组依次异或,最后的结果就是只出现一次的那个数。所以对原数组依次异或,得到的结果是那两个只出现一次的数的异或值,需要找到这个异或值的某位为1的位,根据这个位是否为1将原数组分成两组,对这两组数依次异或得到的两个值就是结果。

220. Contains Duplicate III

题目:给定长度为N的数组,整数K和T,如果可以在数组中找到两个数,下标差小于等于K,值的差的绝对值小于等于T,返回true,否则返回false。

做法:对每个数,需要考察前后下标在K以内的数,但是如果要遍历一次数组,实际需要考察每个数的前K个数里是否有满足条件的数。使用一个大小为K的滑动窗口,在遍历初期窗口大小可以小于K,该窗口内的数需要排序,并且需要经常插入和删除,所以使用map,map默认是排序的。对遍历的当前值A,在map中查找第一个大于等于A-T的数,如果该数与A的差的绝对值值小于等于T,则查找成功,否则维护窗口,继续遍历。

396. Rotate Function

题目:给定长度为N的数组,它有N个旋转数组,用0~N-1对应乘以旋转数组,求最大值。

做法:遍历原数组,记录0~N-1与对应元素的和Sum,以及所有元素的和S,记录初始最大值为Sum,遍历N-1次,每次进行一些操作,修改当前Sum,求与当前最大值的最大值,具体执行的操作示例如下。

0*4 + 1*3 + 2*2 + 3*6 初始值Sum

-1*4 + 0*3 + 1*2 + 2*6 (-= S)

0*4 + 0*3 + 1*2 + 2*6 (+= Ai)

3*4 + 0*3 + 1*2 + 2*6 (+= Ai * (N-1))

一开始i是0,操作完后从1开始,每次对应0*X的位置。

421. Maximum XOR of Two Numbers in an Array

题目:O(N)求非负整数数组取两个数进行异或运算的最大值。

做法:似乎涉及32位整数位运算的这个32的常数不算logN。把每个数按照二进制建二叉前缀树,对数组中的每个数,依次遍历二进制表示的各个位,如果为1,在树中尝试寻找0,如果没有0,就找1,为0则相反,找31次可以得到和该数异或的最大值。

424. Logest Repeating Character Replacement

题目:给一串大写字母和一个数字K,可用任意大写字母替换原串中某字母K次,求替换后最长相同字母子串长。

做法:滑动窗口初始化begin=end=0,同时维护窗口中26个大写字母各有几个。每次遍历先获取一个新的字母,修改对应字母的数量,递增end为后续移动窗口做准备。end-begin是当前窗口大小,end-begin-MAX(26个字母数量)是使得窗口内所有字母相同需要的替换次数,当这个次数大于K时,递增begin,修改对应字母数量。

窗口的初始长度为0,保证不需要替换也可以满足所有字母相同,窗口长度增加时如果使窗口内不满足条件,对于新增的字母,结果子串是否包含它有两种情况,如果不包含,当前的窗口就是从字符串头到这里的最大满足条件的窗口(如果此前有一个大于此窗口P的满足条件的窗口Q,此窗口P的左边缘会大于窗口Q的左边缘,遍历时一定会有当前左边缘与窗口Q左边缘重合的时候,不论当前右边缘是多少,在逐渐递增时一定会获得窗口Q),如果包含,把窗口的头去掉,包含进这个字母,得到的新窗口不一定满足条件(例如ABABACB,K=2,执行到C的时候,会把第一个A去掉,剩下的BABAC不满足条件), 接下来移动窗口是为了找到比现在的窗口大的满足条件的窗口,如果接下来再也找不到更大的窗口,就会输出之前得到的窗口大小,如果某个窗口扩充1格可以满足条件,窗口就会增大1。相当于优化了双重循环遍历所有begin和end,根据串内情况计算是否满足条件,每个begin不需要遍历所有的长度,以某个点开始,如果长度为20的串是满足条件的,则长度1-19的都满足条件,如果已经找到了长度为20的串,下一个begin+1就不需要考察end=begin+1了,直接看begin+20,写成双重循环加break计算量是差不多的。


LeetCode感想

面试考上机,不能用编译器,不能用long long,LeetCode里的第八题atoi,1小时没写完,真惨。我这里显示是5个月前做过,当时写了好久,用的方法也没有规划,速度较慢。当时上机的时候优化了一下,如果能写完速度应该还行。看来做过的题如果没有巩固,下次再碰到不一定能在规定时间内完成,不过要巩固的东西太多了,都掌握还是很困难的。

最近两天刷了一下LeetCode的中等难度题,直接从第二页开始,一天15题,一天17题,其中有一题没想出思路,看了答案。发现有些题目有“Follow up”,如果没有按照更高的要求完成,写起来相对比较容易,后续如果有时间再做一遍,应该要研究一下。现在注意到每次AC都会显示所写的代码在运行时间上打败了多少人,大部分都没办法做到时间最优,经常为了省事多遍历一次,如果靠自己想优化时间感觉很困难,过一遍别人的最优代码吸取一下经验,下一轮可能就能好一点。像那些树和图的题,如果调试的话写起来比较麻烦,其中刚好有很多简单题,直接交的话刷得快一点。好多题都是用递归写的,其中有些题如果不用笔来写,还是挺绕的,部分题目为了避免超时要加一个数组持久化,感觉如果转成动态规划会快一些。目前就一道明显的动态规划,后面应该会有更难的题。

使用Kaggle的步骤

我在参与新类型Kaggle竞赛时采用的步骤如下,在赛程一半的时候进入(很多图像类竞赛没办法用Kernel跑完,可以在Discussion里找github上的代码,或者直接上github搜baseline代码,用谷歌搜比赛名也会出来不少github代码)。

  1. 仔细阅读Overview和Data中的内容。

  2. 下载数据后,对Kernels分数排序,优先查看分数高的和票数高的,如果某个Kernel是fork别人的,先看源头,选择几个采用不同方法的代码复制下来跑,感受下运行时间和效果。

  3. 按照方法名称搜原理,基本看懂这个方法的用途、优缺点、有哪些参数影响较大。

  4. 查看该方法的文档,对照着手上的代码看懂,然后把文档看几遍,里面会有少量技巧和调参方法,然后搜一下他人对参数的理解。

  5. 手工调参,看时间和效果的变化(本地和提交的差距)。

  6. 查看Kernels中的EDA,增加对这些数据的感受,大概记一下数据的分布、范围、意义。

  7. 查看Kernels中fork别人然后改进的代码,找一下哪里的代码变了,分析一下为什么好,是否会过拟合。

  8. 尝试自己想一些预处理方法,或者是特征工程,看效果。

  9. 然后再手工调参,注意观察Discussion里大家的讨论,有时候会有一些灵感,也可以直接根据提示写代码执行。

  10. 在觉得排名还可以时,可以使用多折平均,分数一般都会好一些,而且减少过拟合的概率。

  11. 调参可以使用Grid或者Bayesian Optimization。

  12. 使用不同方法得到相似分数的结果进行平均,通常分数可以高一截。

  13. 尝试stacking,方式很多,有时会过拟合,处理得好可以提高非常多,融合多个方法,多层stacking。

  14. 手工平均可以根据验证集的分数选择合适的权重,也可以根据测试集分数选择权重。注意在选择哪些结果进行平均时,可以计算一下相关系数corr,选择相关系数小并且在测试集表现不错的结果进行平均。

  15. 有些情况下伪标签效果不错。

  16. 选择最终的两个提交很重要,如果测试数据非常多,分布也很均匀(比如正例只有1%就是不均匀),基本上可以尽情过拟合。根据一定逻辑进行后处理风险很大。小测试数据对stacking很敏感,不过直接平均的方法在小数据上一般都还行。

  17. 可以看到这里用的很多方法,在工作环境中是没有的,我猜测优化单模型是主流,而且我在参与过程中花时间最多的也是优化单模型,融合水平很低。如何根据Kaggle中学到的知识解决实际问题,是我非常想知道的。