可信认证之九阴真经二
2.3.6 最低成本联通所有城市(1135_会员)
2.3.6.1 最小生成树
(https://blog.csdn.net/luoshixian099/article/details/51908175)
关于图的几个概念定义:
连通图:在无向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该无向图为连通图。
强连通图:在有向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该有向图为强连通图。
连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。
生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
2.3.7 以图辨树(261_会员)
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2012-2019. All rights reserved.
* Description: 261. 以图判树 securec.h
* Author: w00448446
* Create: 2019-11-14
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#define MAX_VERTEX_NUM 10000
int g_vertex[MAX_VERTEX_NUM];
int GetMin(int a, int b)
{
if (a <= b) {
return a;
}
return b;
}
int FindRoot(int a)
{
while (g_vertex[a] != a) {
a = g_vertex[a];
}
return a;
}
bool validTree(int n, int** edges, int edgesSize, int* edgesColSize)
{
int i;
int a, b;
if (edgesSize != (n - 1)) {
return false;
}
if (edgesSize == 0 && n == 1) {
return true;
}
for (i = 0; i < n; i++) {
g_vertex[i] = i;
}
for (i = 0; i < edgesSize; i++) {
a = FindRoot(edges[i][0]);
b = FindRoot(edges[i][1]);
if (a == b) {
return false;
}
if (edges[i][0] < edges[i][1]) {
g_vertex[edges[i][1]] = a;
} else {
g_vertex[edges[i][0]] = b;
}
}
return true;
}
void TestCase1()
{
int m1[] = {0, 1};
int m2[] = {0, 2};
int m3[] = {0, 3};
int m4[] = {1, 4};
int *g[] = {m1, m2, m3, m4};
int length = sizeof(g) / sizeof(g[0]);
int col[] = {2,2,2,2};
int rslt;
int n = 5;
rslt = validTree(n, g, length, col);
if (rslt == 1) {
printf("t1: pass\n");
}
}
void main()
{
TestCase1();
}
2.3.8 按字典序排列最小的等效字符串(1061_会员)
2.3.9 无向图中连通分量的数目(323_会员)
2.3.10 尽量减少恶意软件的传播(924_困难)
注:将initial视为临时圈长,那么所拥有最大个数的圈,就是目标解。
#include <stdlib.h>
#include <string.h>
#include "securec.h"
#define MAX_LEN 301
int g_preNode[MAX_LEN];
int g_hashInfo[MAX_LEN];
int FindRoot(int pos)
{
int son = pos;
int temp;
if (g_preNode[pos] == -1) {
return pos;
}
while (g_preNode[pos] != -1) {
pos = g_preNode[pos];
}
while (g_preNode[son] != -1) {
temp = g_preNode[son];
g_preNode[son] = pos;
son = temp;
}
return pos;
}
void UnionRoot(int x, int y)
{
int a = FindRoot(x);
int b = FindRoot(y);
if (a != b) {
g_preNode[a] = b;
}
}
int minMalwareSpread(int **graph, int graphSize, int *graphColSize, int *initial, int initialSize)
{
int temp;
int max = 0;
int result;
if (graphSize <= 0 || initialSize <= 0) {
return 0;
}
result = initial[0];
(void)memset_s(g_preNode, MAX_LEN * sizeof(int), -1, MAX_LEN * sizeof(int));
(void)memset_s(g_hashInfo, MAX_LEN * sizeof(int), 0, MAX_LEN * sizeof(int));
// step1:建圈
for (int i = 0; i < graphSize; i++) {
for (int j = 0; j < graphColSize[i]; j++) {
if (graph[i][j] == 1) {
UnionRoot(i, j);
}
}
}
// step2:统计各圈长下挂节点数
for (int i = 0; i < graphSize; i++) {
temp = FindRoot(i);
if (temp != -1) {
g_hashInfo[temp]++;
}
}
// step3:获取initial列表中节点所属圈中圈长下挂节点数最大的节点
for (int i = 0; i < initialSize; i++) {
temp = FindRoot(initial[i]);
if ((temp != -1) && ((max < g_hashInfo[temp]) || ((max == g_hashInfo[temp]) && initial[i] < result))) {
max = g_hashInfo[temp];
result = initial[i];
}
}
return result;
}
void main(void)
{
return;
}
3 九阴真经第三式 :滑动窗口
3.1 代表题目:尽可能使字符串相等(1208)
3.2 滑动窗口介绍
(https://zhuanlan.zhihu.com/p/61564531)滑动窗口法,也叫尺取法(可能也不一定相等,大概就是这样 =。=),可以用来解决一些查找满足一定条件的连续区间的性质(长度等)的问题。由于区间连续,因此当区间发生变化时,可以通过旧有的计算结果对搜索空间进行剪枝,这样便减少了重复计算,降低了时间复杂度。往往类似于“请找到满足xx的最x的区间(子串、子数组)的xx”这类问题都可以使用该方法进行解决。
3.2.1 Leetcode 209. 长度最小的子数组
给定一个含有
n
个正整数的数组和一个正整数
s
,找出该数组中满足其和
≥ s
的长度最小的连续子数组。如果不存在符合条件的连续子数组,返回
0
。示例
:
输入
: s = 7, nums = [2,3,1,2,4,3]
输出
: 2
解释
:
子数组
[4,3]
是该条件下的长度最小的连续子数组。
3.2.2 Leetcode 3. 无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的
最长子串
的长度。
示例
1:
输入
: "abcabcbb"
输出
: 3
解释
:
因为无重复字符的最长子串是
"abc"
,所以其长度为
3
。
示例
2:
输入
: "bbbbb"
输出
: 1
解释
:
因为无重复字符的最长子串是
"b"
,所以其长度为
1
。
示例
3:
输入
: "pwwkew"
输出
: 3
解释
:
因为无重复字符的最长子串是
"wke"
,所以其长度为
3
。
请注意,你的答案必须是
子串
的长度,
"pwke"
是一个子序列,不是子串。
3.2.1 Leetcode 1004. 最大连续1的个数 III
给定一个由若干
0
和
1
组成的数组
A
,我们最多可以将
K
个值从
0
变成
1
。
返回仅包含
1
的最长(连续)子数组的长度。
示例
1
:
输入:
A = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:
6
解释:
[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从
0
翻转到
1
,最长的子数组长度为
6
。
示例
2
:
输入:
A = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:
10
解释:
[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从
0
翻转到
1
,最长的子数组长度为
10
。
3.3 触类旁通
3.3.1 尽可能使字符串相等(1208)的一种解法
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#define MAX_STRING_LEN 100001
int g_num;
char g_stringValue[MAX_STRING_LEN];
void InitStringValue(const char *s, const char *t)
{
char *tmpS = s;
char *tmpT = t;
g_num = 0;
while (*tmpS != 0) {
g_stringValue[g_num] = abs(*tmpS - *tmpT);
g_num++;
tmpS++;
tmpT++;
}
return;
}
int equalSubstring(char *s, char *t, int maxCost)
{
int i, j;
int sum = 0;
int maxCount = 0;
int begin = 0;
InitStringValue(s, t);
for (i = 0; i < g_num; i++) {
sum += g_stringValue[i];
if (sum > maxCost) {
maxCount = (maxCount >= (i - begin)) ? maxCount : (i - begin);
sum -= g_stringValue[begin];
begin++;
}
}
maxCount = (maxCount >= (i - begin)) ? maxCount : (i - begin);
return maxCount;
}
3.3.2 340. 至多包含 K 个不同字符的最长子串(会员_困难)
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2012-2018. All rights reserved.
* Description: 项目 LEET_340_LongestSubstringforKDiffChar 的源文件
* Author: f00485759
* Create: 2019-12-16
*/
#include <stdio.h>
#include <stdbool.h>
#include "securec.h"
#define MAX_HASH_NUM 256
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int g_hash[MAX_HASH_NUM];
void Init(void)
{
(void)memset(g_hash, 0, sizeof(g_hash));
}
void HashChar(char c)
{
g_hash[c]++;
}
void DeHashChar(char c)
{
if (g_hash[c] > 0) {
g_hash[c]--;
}
}
bool IsExtendKChar(int k)
{
int count = 0;
for (int i = 0; i < MAX_HASH_NUM; i++) {
if (g_hash[i] != 0) {
count++; // 可以通过变量来记忆,减少计算量
}
if (count >= k) {
return true;
}
}
return false;
}
bool IsRepeat(char c)
{
if (g_hash[c] != 0) {
return true;
}
return false;
}
int lengthOfLongestSubstringKDistinct(const char *s, int k)
{
int start, end, maxlen;
int slen = (int)strlen(s);
Init();
start = 0;
maxlen = 0;
for (end = 0; end < slen; end++) {
if (!IsExtendKChar(k) || IsRepeat(s[end])) {
HashChar(s[end]);
continue;
}
if (start == end) {
break;
}
maxlen = MAX(maxlen, end - start);
DeHashChar(s[start]);
start++;
while (IsExtendKChar(k)) {
DeHashChar(s[start]);
start++;
}
HashChar(s[end]);
}
maxlen = MAX(maxlen, end - start);
return maxlen;
}
int main()
{
const char *s = "a";
int k = 0;
int maxlen = lengthOfLongestSubstringKDistinct(s, k);
printf("%d\n", maxlen);
return 0;
}
3.3.3 1151. 最少交换次数来组合所有的 1(会员)
3.3.4 159. 至多包含两个不同字符的最长子串(会员)
3.3.5 1100. 长度为 K 的无重复字符子串(会员)
#define MAX_LEN 512
int g_array[MAX_LEN];
int numKLenSubstrNoRepeats(char * s, int k){
int right = 0;
int left = 0;
int result = 0;
int count = 0;
int len;
len = strlen(s);
memset(g_array, 0, sizeof(g_array));
while (right < len) {
if (g_array[s[right]] == 0) {
count++;
}
g_array[s[right]]++;
while (right - left + 1 > k) {
g_array[s[left]]--;
if (g_array[s[left]] == 0) {
count--;
}
left++;
}
printf("%d, %d, %d\n", right, left, count);
if ((right - left + 1 == k) && (count == k)) {
result++;
}
right++;
}
return result;
}
4 九阴真经第四式 :前缀和 & HASH
4.1 代表题目:560. 和为K的子数组
注:前缀和+hash,可以在O(n)复杂度来解决此问题。
4.2 前缀和介绍
前缀和(Prefix Sum)定义(https://www.jianshu.com/p/3021429f38d4):
给定一个数组A[1..n],前缀和数组PrefixSum[1..n]定义为:PrefixSum[i] = A[0]+A[1]+...+A[i-1];
例如:A[5,6,7,8] --> PrefixSum[5,11,18,26]
PrefixSum[0] =A[0] ;
PrefixSum[1] =A[0] + A[1] ;
PrefixSum[2] =A[0] + A[1] + A[2] ;
PrefixSum[3] =A[0] + A[1] + A[2] + A[3] = PrefixSum[2] + A[3] ;
4.3 触类旁通
4.3.1 523. 连续的子数组和
注:《= O(n^2)复杂度解决
// 使用Hash O(n)复杂度,因没有进行动态内存申请,所以空间占用较大,留给大家进一步优化
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#define MAX_HASH_LEN 20001
#define MAX_HASH_NODE_LEN (MAX_HASH_LEN + MAX_HASH_LEN + 1)
typedef struct {
int valid;
int value;
int count;
} HashNode;
HashNode g_hashMap[MAX_HASH_NODE_LEN];
int g_max;
int GetHashCode(int val)
{
int hashCode = (abs(val) * 131) % MAX_HASH_LEN;
return hashCode;
}
int GetHashMapIndex(int val)
{
int hashCode;
hashCode = GetHashCode(val);
for (int i = hashCode; i < MAX_HASH_NODE_LEN; i++) {
if (g_hashMap[i].valid == 0) {
return i;
}
if (g_hashMap[i].value == val) {
return i;
}
}
// 异常保护
printf("GetHashMapIndex:error\n");
return 0;
}
void UpdateHashMap(int index, int val)
{
if (g_hashMap[index].valid == 0) {
g_hashMap[index].valid = 1;
g_hashMap[index].value = val;
g_hashMap[index].count = 1;
return;
}
if (g_hashMap[index].value != val) {
printf("UpdateHashMap: error\n");
return;
}
g_hashMap[index].count++;
return ;
}
void Init()
{
g_max = 0;
for (int i = 0; i < MAX_HASH_NODE_LEN; i++) {
g_hashMap[i].valid = 0;
g_hashMap[i].count = 0;
g_hashMap[i].value = 0;
}
// 将0初始化为有效值,但初始计数为0
g_hashMap[0].valid = 1;
g_hashMap[0].count = 1;
}
int subarraySum(int* nums, int numsSize, int k)
{
int index;
int sum = 0;
int preSum = 0;
Init();
for (int i = 0; i < numsSize; i++) {
sum += nums[i];
// 获取preSum的个数
preSum = sum - k;
index = GetHashMapIndex(preSum);
if (g_hashMap[index].valid == 1) {
g_max += g_hashMap[index].count;
}
// 获取HashIndex
index = GetHashMapIndex(sum);
// 放入Hash表中
UpdateHashMap(index, sum);
}
return g_max;
}
void Test1()
{
int nums[] = {1,1,1};
int numsSize = sizeof(nums) / sizeof(nums[0]);
int k = 2;
int rslt;
rslt = subarraySum(nums, numsSize, k);
if (rslt == 2) {
printf("Test1 pass \n");
} else {
printf("Test1 fail \n");
}
}
void main()
{
Test1();
return;
}
4.3.2 974. 和可被 K 整除的子数组
5 九阴真经第五式 :差分
5.1.1 代表题目:1094. 拼车
5.2 差分介绍
对于数组array[N]中的某一段进行增减操作,通过差分可在O(n)时间内完成。如
trips = [[2,1,5],[3,3,7]]
第一步:更新array[1] = 2, array[5] = -2;
第二步:更新array[3] = 3, array[7] = -3;
第三步:进行求和,得到结果array[] = {0,2,2,5,5,3,3,0};
5.3 触类旁通
5.3.1 1109. 航班预订统计
5.3.2 121. 买卖股票的最佳时机(简单)
5.3.3 122. 买卖股票的最佳时机 II
int maxProfit(int *prices, int pricesSize)
{
int purchasePrice;
int profit = 0;
if (prices == NULL || pricesSize <= 0) {
return 0;
}
purchasePrice = prices[0];
for (int i = 1; i < pricesSize; i++) {
if (purchasePrice < prices[i]) {
profit += (prices[i] - purchasePrice);
}
purchasePrice = prices[i];
}
return profit;
}
5.3.4 253. 会议室 II (会员)
#include <stdio.h>
#include <securec.h>
#define MAX_LEN 1000000
int g_hashInfo[MAX_LEN];
int minMeetingRooms(int **intervals, int intervalsSize, int *intervalsColSize)
{
int max = 0;
int sum = 0;
if (intervals == NULL || intervalsSize <= 0 || intervalsColSize == NULL) {
return 0;
}
(void)memset_s(g_hashInfo, MAX_LEN * sizeof(int), 0, MAX_LEN * sizeof(int));
for (int i = 0; i < intervalsSize; i++) {
g_hashInfo[intervals[i][0]]++;
g_hashInfo[intervals[i][1]]--;
}
for (int i = 0; i < MAX_LEN; i++) {
sum += g_hashInfo[i];
if (max < sum) {
max = sum;
}
}
return max;
}
6 九阴真经第六式 :拓扑排序(专业级)
6.1 代表题目:210. 课程表 II
6.2 拓扑排序介绍
(https://blog.csdn.net/lisonglisonglisong/article/details/45543451)
一、什么是拓扑排序
在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:
每个顶点出现且只出现一次。
若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说。
例如,下面这个图:
它是一个 DAG 图,那么如何写出它的拓扑排序呢?这里说一种比较常用的方法:
从 DAG 图中选择一个 没有前驱(即入度为0)的顶点并输出。
从图中删除该顶点和所有以它为起点的有向边。
重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。
于是,得到拓扑排序后的结果是 { 1, 2, 4, 3, 5 }。通常,一个有向无环图可以有一个或多个拓扑排序序列。
二、拓扑排序的应用
拓扑排序通常用来“排序”具有依赖关系的任务。比如,如果用一个DAG图来表示一个工程,其中每个顶点表示工程中的一个任务,用有向边<A,B><A,B>表示在做任务 B 之前必须先完成任务 A。故在这个工程中,任意两个任务要么具有确定的先后关系,要么是没有关系,绝对不存在互相矛盾的关系(即环路)。
6.3 触类旁通
6.3.1 课程表II(210)的一种解法
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <ctype.h>
typedef struct {
int num;
int *array;
} Node;
int *g_dest;
int g_destLen;
int g_front;
Node *g_preNode;
Node *g_outNode;
int GetMax(int a, int b)
{
if (a >= b) {
return a;
}
return b;
}
bool Init(int numCourses, int **prerequisites, int prerequisitesSize, int *prerequisitesColSize)
{
int i;
int input;
int output;
g_preNode = (Node *)malloc(sizeof(Node) * numCourses);
if (g_preNode == NULL) {
return false;
}
g_outNode = (Node *)malloc(sizeof(Node) * numCourses);
if (g_outNode == NULL) {
return false;
}
for (i = 0; i < numCourses; i++) {
g_preNode[i].num = 0;
g_preNode[i].array = NULL;
g_outNode[i].num = 0;
g_outNode[i].array = NULL;
}
for (i = 0; i < prerequisitesSize; i++) {
input = prerequisites[i][1];
output = prerequisites[i][0];
g_preNode[output].num++;
g_outNode[input].num++;
}
// 动态分配数组
for (i = 0; i < numCourses; i++) {
if (g_preNode[i].num > 0) {
g_preNode[i].array = (int *)malloc(sizeof(int) * g_preNode[i].num);
if (g_preNode[i].array == NULL) {
return false;
}
g_preNode[i].num = 0;
}
if (g_outNode[i].num > 0) {
g_outNode[i].array = (int *)malloc(sizeof(int) * g_outNode[i].num);
if (g_outNode[i].array == NULL) {
return false;
}
g_outNode[i].num = 0;
}
}
for (i = 0; i < prerequisitesSize; i++) {
input = prerequisites[i][1];
output = prerequisites[i][0];
g_preNode[output].array[g_preNode[output].num] = input;
g_preNode[output].num++;
g_outNode[input].array[g_outNode[input].num] = output;
g_outNode[input].num++;
}
g_dest = (int *)malloc(numCourses * sizeof(int));
if (g_dest == NULL) {
return false;
}
g_destLen = 0;
g_front = 0;
return true;
}
void FreeNode(int numCourses)
{
int i;
if (g_preNode != NULL) {
for (i = 0; i < numCourses; i++) {
if (g_preNode[i].array != NULL) {
free(g_preNode[i].array);
}
}
free(g_preNode);
g_preNode = NULL;
}
if (g_outNode != NULL) {
for (i = 0; i < numCourses; i++) {
if (g_outNode[i].array != NULL) {
free(g_outNode[i].array);
g_outNode[i].array = NULL;
}
}
free(g_outNode);
g_outNode = NULL;
}
}
void Bfs(int numCourses)
{
int i;
int curr;
int out;
for (i = 0; i < numCourses; i++) {
if (g_preNode[i].num == 0) {
g_dest[g_destLen++] = i;
}
}
while (g_front < g_destLen) {
curr = g_dest[g_front];
g_front++;
for (i = 0; i < g_outNode[curr].num; i++) {
out = g_outNode[curr].array[i];
// 删除, 为0,则进入BFS队列
g_preNode[out].num--;
if (g_preNode[out].num == 0) {
g_dest[g_destLen] = out;
g_destLen++;
}
}
}
}
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int *findOrder(int numCourses, int **prerequisites, int prerequisitesSize, int *prerequisitesColSize,
int *returnSize)
{
bool rslt;
rslt = Init(numCourses, prerequisites, prerequisitesSize, prerequisitesColSize);
if (!rslt) {
FreeNode(numCourses);
(*returnSize) = 0;
return NULL;
}
Bfs(numCourses);
FreeNode(numCourses);
(*returnSize) = g_destLen;
if (g_destLen < numCourses) {
(*returnSize) = 0;
}
return g_dest;
}
void TestCase1()
{
int m1[] = { 1, 0 };
int *g[] = { m1 };
int prerequisitesColSize[] = { 2 };
int numCourses = 2;
int prerequisitesSize = 1;
int returnSize;
int *rslt;
rslt = findOrder(numCourses, g, 1, prerequisitesColSize, &returnSize);
if (returnSize == 2) {
printf("1: pass");
}
}
void TestCase2()
{
int m1[] = { 1, 0 };
int m2[] = { 2, 0 };
int m3[] = { 3, 1 };
int m4[] = { 3, 2 };
int *g[] = { m1, m2, m3, m4 };
int prerequisitesColSize[] = { 2 };
int numCourses = 4;
int prerequisitesSize = 4;
int returnSize;
int *rslt;
rslt = findOrder(numCourses, g, 1, prerequisitesColSize, &returnSize);
if (returnSize == 4) {
printf("1: pass");
}
}
int main()
{
TestCase2();
TestCase1();
return 0;
}
6.3.2 444. 序列重建 (会员)
注:进行拓扑排序:
(5,2),(2,6),(6,3),(4,1),(1,5),(5,2)
6.3.3 269. 火星词典
7 九阴真经第七式 :字符串
7.1 代表题目:5. 最长回文子串
7.2 字符串介绍
字符串可以涉及非常多的考点:如递归、栈、hash、dfs、bfs、动态规划等,需要重点关注。
7.3 触类旁通
7.3.1 93. 复原IP地址(dfs)
7.3.2 43. 字符串相乘
7.3.3 227. 基本计算器 II
给出一个表达式,计算结果,就是一个计算器类型的问题,也是栈的经典应用。
栈:添加和删除元素都在队尾进行,先进后出,后进先出,类比于子***,入栈和出栈。
思路:
将表达式(中缀)转化为后缀
将后缀计算出结果
具体规则为:
1.中缀转后缀:
- 数字直接输出到后缀表达式
- 栈为空时,遇到运算符,直接入栈
- 遇到运算符,弹出所有优先级大于或等于该运算符的栈顶元素,并将该运算符入栈
- 将栈中元素依次出栈
例如:表达式:3+2 * 2
遇到3,直接输出到后缀表达式中,栈中元素为空,结果为: 栈: 空; 后缀表达式:3
遇到符号“+”,入栈,结果为: 栈:+ ; 后缀表达式:3
遇到2,直接输出,结果为: 栈:+; 后缀表达式: 3 2
遇到乘号*,入栈,结果为: 栈: * + ;后缀表达式:3 2
遇到2,直接输出,结果为: 栈: * + ;后缀表达式:3 2 2
最后,将元素出栈:结果为:后缀表达式:3 2 2 * +
2.计算后缀:
- 遇到数字,入栈
- 遇到运算符,弹出栈顶两个元素,做运算,并将结果入栈
- 重复上述步骤,直到表达式最右端
例如上述得到的后缀表达式为 3 2 2 * +
3 2 2 都是数字,入栈,结果为:栈:2 2 3
遇到* 号, 2 2 出栈,并计算,2*2 = 4, 4入栈,结果为:栈:4 3 ,表达式还剩一个加号
遇到+ 号,栈顶两个元素出栈并运算,4 3 做加法,4+3 =7
后缀表达式空了,计算完毕,输出结果:7
8 九阴真经第八式 :BFS
8.1 代表题目:127. 单词接龙
8.2 Bfs介绍
(https://zhuanlan.zhihu.com/p/62884729)
BFS(广度优先搜索) 常用来解决最短路径问题。第一次遍历到目的节点时,所经过的路径是最短路径。几个要点:
只能用来求解无权图的最短路径问题
队列:用来存储每一层遍历得到的节点
标记:对于遍历过的结点,应将其标记,以防重复访问。
注:
- 1. 广度搜索时候,如果曾经加入过,后续就不用再加入了;
- 2. 加入队列时候,需要标记当前层级,方便后续直接返回目标解;
8.3 触类旁通
8.3.1 139. 单词拆分
8.3.2 130. 被围绕的区域
8.3.3 317. 离建筑物最近的距离(困难)
8.3.4 505. 迷宫 II (会员)
8.3.5 529. 扫雷游戏
8.3.6 1263. 推箱子(困难)
8.3.7 1197. 进击的骑士
8.3.8 815. 公交路线(困难_必做)
8.3.9 934. 最短的桥
9 九阴真经第九式 :DFS
9.1 代表题目:934. 最短的桥
9.2 Dfs介绍
DFS实质就是一种枚举,不过借助递归实现;
DFS的基本模式:
void dfs(int step)
{
判断边界;
for(int i=1;i<=n;++i)//尝试每一种可能;
{
dfs(step+1);/‘/继续下一步
}
返回;
}
9.3 触类旁通
9.3.1 1102. 得分最高的路径
9.3.2 685. 冗余连接 II(困难)
9.3.3 531. 孤独像素 I
9.3.4 533. 孤独像素 II
9.3.5 332. 重新安排行程
9.3.6 337. 打家劫舍 III
9.3.7 113. 路径总和 II
10 九阴真经第十式 :动态规划
10.1 代表题目:213. 打家劫舍 II
10.2 动态规划介绍
https://blog.csdn.net/qq_37763204/article/details/79394397
https://blog.csdn.net/u013309870/article/details/75193592
10.3 触类旁通
10.3.1 1043. 分隔数组以得到最大和
10.3.2 416. 分割等和子集
10.3.3 123. 买卖股票的最佳时机 III
一、穷举框架
首先,还是一样的思路:如何穷举?这里的穷举思路和上篇文章递归的思想不太一样。
递归其实是符合我们思考的逻辑的,一步步推进,遇到无法解决的就丢给递归,一不小心就做出来了,可读性还很好。缺点就是一旦出错,你也不容易找到错误出现的原因。比如上篇文章的递归解法,肯定还有计算冗余,但确实不容易找到。
而这里,我们不用递归思想进行穷举,而是利用「状态」进行穷举。我们具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。听起来抽象,你只要记住「状态」和「选择」两个词就行,下面实操一下就很容易明白了。
Python
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
比如说这个问题,每天都有三种「选择」:买入、卖出、无操作,我们用 buybuy,sellsell,restrest 表示这三种选择。但问题是,并不是每天都可以任意选择这三种选择的,因为 sellsell 必须在 buybuy 之后,buybuy 必须在 sellsell 之后。那么 restrest 操作还应该分两种状态,一种是 buybuy 之后的 restrest(持有了股票),一种是 sellsell 之后的 restrest(没有持有股票)。而且别忘了,我们还有交易次数 kk 的限制,就是说你 buybuy 还只能在 k > 0 的前提下操作。
很复杂对吧,不要怕,我们现在的目的只是穷举,你有再多的状态,老夫要做的就是一把梭全部列举出来。这个问题的「状态」有三个,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的 restrest 的状态,我们不妨用 11 表示持有,00 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:
Python
dp[i][k][0 or 1]
0 <= i <= n-1, 1 <= k <= K
n 为天数,大 K 为最多交易数
此问题共 n × K × 2 种状态,全部穷举就能搞定。
for 0 <= i < n:
for 1 <= k <= K:
for s in {0, 1}:
dp[i][k][s] = max(buy, sell, rest)
而且我们可以用自然语言描述出每一个状态的含义,比如说 dp[3][2][1] 的含义就是:今天是第三天,我现在手上持有着股票,至今最多进行 22 次交易。再比如 dp[2][3][0] 的含义:今天是第二天,我现在手上没有持有股票,至今最多进行 33 次交易。很容易理解,对吧?
我们想求的最终答案是 dp[n - 1][K][0],即最后一天,最多允许 K 次交易,最多获得多少利润。读者可能问为什么不是 dp[n - 1][K][1]?因为 [1] 代表手上还持有股票,[0] 表示手上的股票已经卖出去了,很显然后者得到的利润一定大于前者。
记住如何解释「状态」,一旦你觉得哪里不好理解,把它翻译成自然语言就容易理解了。
二、状态转移框架
现在,我们完成了「状态」的穷举,我们开始思考每种「状态」有哪些「选择」,应该如何更新「状态」。只看「持有状态」,可以画个状态转移图。
通过这个图可以很清楚地看到,每种状态(0 和 1)是如何转移而来的。根据这个图,我们来写一下状态转移方程:
Python
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 选择 rest , 选择 sell )
解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )
解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。
这个解释应该很清楚了,如果 buybuy,就要从利润中减去 prices[i]prices[i],如果 sellsell,就要给利润增加 prices[i]prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 kk 的限制,我们在选择 buybuy 的时候,把 kk 减小了 11,很好理解吧,当然你也可以在 sellsell 的时候减 11,一样的。
现在,我们已经完成了动态规划中最困难的一步:状态转移方程。如果之前的内容你都可以理解,那么你已经可以秒杀所有问题了,只要套这个框架就行了。不过还差最后一点点,就是定义 base case,即最简单的情况。
Python
dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。
dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。
把上面的状态转移方程总结一下:
Python
base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
读者可能会问,这个数组索引是 -1 怎么编程表示出来呢,负无穷怎么表示呢?这都是细节问题,有很多方法实现。现在完整的框架已经完成,下面开始具体化。
10.3.4 62. 不同路径
10.3.5 63. 不同路径 II
10.3.6 651. 4键键盘(会员)
10.3.7 361. 轰炸敌人
10.3.8 1066. 校园自行车分配 II(会员)
10.3.9 750. 角矩形的数量(会员)
10.3.10 1230. 抛掷硬币
10.3.11 1055. 形成字符串的最短路径(会员)
11 九阴真经第十一式 :贪心算法
11.1.1 代表题目:452. 用最少数量的箭引爆气球
注:排序后,第一箭能射穿所有end《= start的球。
11.2 贪心算法介绍
11.3 触类旁通
11.3.1 1231. 分享巧克力(会员)
11.3.2 1247. 交换字符使得字符串相同
11.3.3 45. 跳跃游戏 II
11.3.4 621. 任务调度器
11.3.5 376. 摆动序列
12
九阴真经第十二式
:字典树
12.1 代表题目:820. 单词的压缩编码
注:本题可信考了两次。
解法二:
直接按前序进行排序,如果第一个在第二个里面,则忽略第一个。 如:em emit,则忽略em。
12.2 字典树介绍
12.3 触类旁通
12.3.1 1231. 分享巧克力(会员)
12.3.2 648. 单词替换
12.3.3 208. 实现 Trie (前缀树)
本文来自博客园,作者:易先讯,转载请注明原文链接:https://www.cnblogs.com/gongxianjin/p/16495749.html