Neo's Blog

不抽象就无法深入思考
不还原就看不到本来面目!

0%

多重背包问题 背包问题系列
问题描述 给定N个物品,其中第i种物品的体积为$V_i$,价值为$W_i$,并且有$C_i$个。有一个容积为M的背包,要求选择一些物品放入背包,使得物品总体积不超过M的前提下,物品总价值和最大。
问题转换 将第i种物品看作独立的$C_i$个物品,从而转换为共有$\sum\limits_{i=1}^{N}C_i$个物品的0/1背包问题
问题转换 将数量为$C_i$的第i个物品拆成p+2个物品,他们的体积分别为:$2^0V_i,2^1V_i,\dots,2^pV_i,R_iV_i$,其中$R_i=C_i-2^0-2^1-\dots-2^p$
问题优化 可借助单调队列优化

DP概念

动态规划是运筹学的一个分支,是一种求解多阶段决策过程最优化问题的数学方法。要搞清楚它,首先需要搞清楚几个基本概念。

阶段:整个决策过程可以按某个(空间、时间或其它)标准分为多个步骤,每一步称为一个阶段。比如下棋,走一步就可以认为是一个阶段。

状态:状态表示在每个阶段我们关注的决策相关的影响因素的情况。比如下棋到某一步时,此刻棋盘上所有棋子的位置就是此阶段的状态。状态通常可以用一个或多个变量来描述。

决策:一个阶段的状态给定以后,从该状态演变到下一阶段某个状态的一种选择行动方法称为决策。比如下棋到某一步时,决定下一步怎么走,就是一步决策,也叫单阶段决策。决策通常也可以用变量来描述。

策略:由每个阶段的决策组成的序列称为策略,也就是多阶段决策。比如下棋,从开始下到结束的每一步走法构成了一个策略。策略可能有很多种,其中达到最优效果的策略称为最优策略。

状态转移方程:从一个阶段到下一个阶段的状态变化,称为状态转移,如果这个变化可以用方程来描述,则称之为状态转移方程。

无后效性:说的是一旦某个阶段的状态确定,则此后过程的演变不再受此前各种状态及决策的影响。也就是说历史信息不会影响我们以后的决策。比如下棋,现在已经有一个棋面,有可能是随机摆成的,也可能是认真下成这样的,但是不管怎样这不影响我们去决定下一步应该怎么下。当前的棋面是唯一影响未来的东西。

最优⼦结构:如果每个阶段的最优状态可以从之前某个或某些阶段的某个或某些状态直接得到,也就是说一个问题的最优解可以由其子问题的最优解来得到,我们称其具有最优子结构。

重叠⼦问题:在求解一个问题时需要先求解其子问题,子问题又有子问题,若在求解过程中需要重复求解某些子问题,这些子问题称为重叠子问题。对于重叠子问题不需要重复求解,只需要求解一次,然后记录下来,以后每次查询就可以了,可以大大降低运行时间,即用空间换时间。

参考:https://blog.csdn.net/tyst08/article/details/106412008

简单起见,下面用大家非常熟悉的求斐波拉契数列的过程来说明这几个概念。斐波拉契数列的公式为:

f(1)=f(2)=1

f(n)=f(n−1)+f(n−2)(n≥3)

假设我现在想计算第10个非波那契数,那么计算每一个斐波拉契数的过程就是一个阶段,每一个斐波拉契数就是这个问题的一个状态。按照公式计算就是决策。每一步都按公式算就是策略。状态转移方程为 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n-1) + f(n-2)f(n)=f(n−1)+f(n−2)。求一个新数字只需要之前的两个状态,与怎么得到这个状态值无关,所以具有无后效性。每个阶段的状态即斐波拉契数可以由之前的两个状态得到,所以具有最优子结构。根据公式,求 f ( n ) f(n)f(n) 时需要求 f ( n − 2 ) f(n-2)f(n−2),求 f ( n − 1 ) f(n-1)f(n−1) 时也需要求 f ( n − 2 ) f(n-2)f(n−2),所以有重叠⼦问题。

动态规划就是利用最优子结构,把多阶段决策过程的最优化问题转化为一系列单阶段最优化问题,然后逐个求解子问题。在每一个子问题的求解中,均可以利用了它前面的子问题的最优化结果,依次进行,直到得到最后一个子问题的最优解,也就是整个问题的最优解。

动态规划的流程一般可以分为以下几步:

  1. 对决策过程划分阶段。
  2. 对各阶段确定状态变量。
  3. 根据状态变量确定每个决策的开销以及最终的目标函数。
  4. 建立各阶段状态变量的转移过程,确定状态转移方程。
  5. 根据状态转移方程用代码来实现,一般可以用递归,注意对重叠子问题要记录其解。

动态规划优化策略

  1. 费用提前/延后计算:没有关于某个维度的限制条件,这个维度纯粹用来计算费用,考虑是否可以提前计算
  2. 空间压缩

经典问题

常见的动态规划问题有哪些:

打家劫舍(普通版本)
问题描述 不能打结连续的两家人
状态表示 F[i]表示前 i 间房屋能偷窃到的最高总金额
阶段划分 空间特征,子序列的结尾位置(数列A中的位置,从前到后)
转移方程 $F[i]=max(F[i-1], F[i-2]+A[i])$
边界 F[0]= 0
目标 $F[N]$
优化点 只跟前2有关系,滚动数组
相关题目
打家劫舍(环形数组)
问题描述 不能打结连续的两家人
状态表示 F[i]表示前 i 间房屋能偷窃到的最高总金额
阶段划分 空间特征,子序列的结尾位置(数列A中的位置,从前到后)
转移方程 $F[i]=max(F[i-1], F[i-2]+A[i])$
边界 F[0]= 0
目标 $F[N]$
优化点 连续两次普通版本,分别是打结第一家 OR 不打结第一家
相关题目
打家劫舍(二叉树版本)
问题描述
状态表示 dp[HASH_KEY] = MONEY
阶段划分
转移方程
边界
目标 二叉树遍历 + 记忆化搜索加速
相关题目
LIS(Longest Increasing Subsequence)问题
问题描述 最长上升子序列。给定一个长度为N的数列A,求数值单调递增的子序列的长度最长是多少。A的任意子序列B可表示为$B={A_{k_1},A_{k_2},A_{k_3},\ldots,A_{k_p}}$,其中$k_1 < k_2 <\ldots<k_p$
状态表示 F[i]表示以A[i]为结尾的最长上升子序列的长度
阶段划分 空间特征,子序列的结尾位置(数列A中的位置,从前到后)
转移方程 $F[i]=\underset{0 \le j \lt i,A[j] \lt A[i]}{\mathrm{max}}\{F[j] + 1\}$ 需要遍历j
边界 F[0]= 0
目标 $\underset{1 \le i \le N}{\mathrm{max}}{F[i]}$
相关题目 俄罗斯信封套娃问题
LCS问题
问题描述 最长公共子序列。给定两个长度分别为N和M的字符串A和B,求即是A的子序列又是B的子序列的字符串长度最长是多少。
状态表示 F[i,j]表示前缀子串A[1-i]与B[1-j]的最长公共子序列的长度
阶段划分 空间特征,已经处理的前缀长两个字符串中的位置,即一个二维坐标
转移方程 $F[i,j]=\begin{cases}F[i,j-1] \\ F[i-1,j] \\ F[i-1,j-1] + 1 & \text{if } A[i] = B[j] \end{cases}$
边界 F[i,0]=F[0,j]=0
目标 $F[N,M]$
数字三角形
问题描述 给定一个共有N行的三角矩阵A,其中第i行有i列。从左上角出发,每次可以向下方或者右下方走一步,最终到达底部。求把经过的所有位置上的数加起来,和最大是多少。
状态表示 F[i,j]表示从左上角走到第i行,第j列,和最大是多少
阶段划分 空间特征,路径的结尾位置(矩阵中的行列位置),即一个二维坐标
转移方程 $F[i,j]=A[i,j]+\begin{cases}F[i-1,j] \\ F[i-1,j-1] & \text{if } j \gt 1 \end{cases}$
边界 F[1,1]=A[1,1]
目标 $\underset{1 \le j \le N}{\mathrm{max}}{F[N,j]}$
数字组合(01背包模型) 背包问题系列
问题描述 $A_1,A_2,A_3,\dots,A_N$,权重分别为$W_1,W_2,W_3,\dots,W_N$
状态表示 F[i,j]表示使用前i个数字拼凑出体积为j的物品的最大权重
阶段划分 主阶段:已经选择的数字,拼凑出的和是附加维度
转移方程 $F[i,j]=max(F[i-1,j], F[i-1,j-A_i] + W_i)$ 第一项是不选择第i个数字,第二项是选择第i个数字
边界 F[0,0]=1,其余为0
目标 F[N,M]
空间优化 调整遍历顺序(V从大到小),确保可以用滚动数组,不会覆盖前值
相关题目 划分数组为两个相等的子集
数字组合(完全背包模型) 背包问题系列
问题描述 $A_1,A_2,A_3,\dots,A_N$,权重分别为$W_1,W_2,W_3,\dots,W_N$
状态表示 F[i,j]表示使用前i个数字拼凑出体积为j的物品的最大权重
阶段划分 主阶段:已经选择的数字,拼凑出的和是附加维度
【优化前】转移方程 $F[i,j]=max{F[i,j- k A_i] + k W_i, k * A_i < V}$ 第一项是不选择第i个数字,第二项是选择第i个数字
【优化后】转移方程 $F[i,j]=max(F[i-1,j], F[i,j-A_i])$ 第一项是不选择第i个数字,第二项是选择第i个数字
边界 F[0,0]=1,其余为0
目标 F[N,M]
空间优化 调整遍历顺序(V从小到大),确保可以用滚动数组,不会覆盖前值
数字组合(多重背包模型) 背包问题系列
问题描述 $A_1,A_2,A_3,\dots,A_N$,权重分别为$W_1,W_2,W_3,\dots,W_N$,物品数量不超过$N_1,N_2,N_3,\dots,N_N$
状态表示 F[i,j]表示使用前i个数字拼凑出体积为j的物品的最大权重
阶段划分 主阶段:已经选择的数字,拼凑出的和是附加维度
【优化前】转移方程 $F[i,j]=max{F[i,j- k A_i] + k W_i, k < N_i}$ 第一项是不选择第i个数字,第二项是选择第i个数字
【优化后】转移方程 把$N_i$拆分为二进制,然后转换为01背包问题
边界 F[0,0]=1,其余为0
目标 F[N,M]
空间优化 调整遍历顺序(V从小到大),确保可以用滚动数组,不会覆盖前值
最少硬币数 换硬币系列
问题描述 给定N种硬币,其中第i种面额为$C_i$,计算凑成总金额M所需的最少的硬币个数。每种硬币的数量是无限的。
状态表示 F[i]表示凑出金额i用的最小硬币数
转移方程 $F[i]=\underset{1 \le j \le N}{\mathrm{min}}{F[i - C_j] + 1}$
边界 F[0]=0,其余为正无穷
目标 F[M]
求硬币拼凑的总方案数 换硬币系列
问题描述 给定N种硬币,其中第i种面额为$C_i$,计算凑成总金额M的所有方案数
状态表示 F[i,m]表示利用前i种硬币凑出金额m的方案数
转移方程 $F[i]=\sum\limits_{j=1}^{N} {F[i - C_j]}$
边界 F[0]=1,其余为0
目标 F[M]
最大子数组
问题描述 子数组和最大
状态表示 F[i]表示利用以d[i]结尾的子数组的最大和
转移方程 $F[i]= max(F[i-1] + A_i, A_i)$,其实等价于判断F[i-1]是否小于0
边界 F[0]=1,其余为0
目标 F[M]
相关题目 乘积最大连续子数组\最大子矩阵:i~j列上的数字进行加和,然后二维转一维
相关题目 环形最大子数组:Copy一遍,长度*2,然后限制长度不超过n
相关题目 环形最大子数组:max(【跨末端】Sum - 最小子数组, 【原】最大子数组)
股票买卖
问题描述 交易K次
状态表示 F[Day, MaxTradeCnt, FullOrShort]表示利用前Day天,交易次数剩余MaxTradeCnt, 空仓满仓时能够获得的最大利润
转移方程 $F[i,k,0]=max(F[i-1, k, 0], F[i-1,k,1] + prices[i])$
转移方程 $F[i,k,1]=max(F[i-1, k, 1], F[i-1,k-1,0] - prices[i])$
边界 F[0]=1,其余为0
目标 F[M]
相关题目 交易一次,交易无限次,交易K次
字符串编辑距离问题
问题描述 strA与strB变为一样所需要的最少操作次数
状态表示 L(i,j)为使两个字符串和Ai和Bj相等的最小操作次数;Ai表示前i个字符
转移方程 $L(i,j) = min( L(i-1,j-1), L(i-1,j), L(i,j-1) ) + 1$
最长回文串
问题描述
状态表示 F[i][j] 表示 S[i] 至 S[j] 所表示的子串是否是回文子串
转移方程 $F[i,j]=\begin{cases}F[i+1,j-1] \text{if } S_i = S_j \\ 0 & \text{if } S_i \ne S_j \end{cases}$
扔鸡蛋问题
问题描述 dp[i,j]表示i层,j个鸡蛋,最坏情况下的最少扔鸡蛋次数
转移方程 $dp(N,K)=\underset{1 \le i \le N}{\mathrm{min}}(1+max(dp(N-i,K), dp(i-1,K-1)))$
具体实现 状态扭转特别复杂,用for循环有点困难,所以我们采用DFS + 记忆化搜索,时间复杂度:$O(NNK)$;上面方程中的i啥意思?他表示对于要探测的N层,有N种选择;我需要挨个去做选择,而不是在枚举状态;状态的枚举是通过DFS递归来实现的
二分搜索优化 dp(N-i,K)与dp(i-1,k-1) 总是一个单调递增,一个单调递减;所以什么时候最小呢?所以两者的直线交点即为最小值;这种思路类似于峰谷问题,可以用二分解决!把时间复杂度将为$O(N K LgN)$
扔鸡蛋问题-进阶
问题描述 F[i,j]表示最多允许i个鸡蛋,当前已经扔了j个,能确定的最高楼层数
转移方程 $dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1$

动态规划相关的经典题目

题目分类 题目名称 考察点 其他说明
二维DP 棋盘上获取最大价值的礼物
字符串DP 获取不同的翻译方法次数
二维DP 求解一个矩阵中找一条最长的递增路径
二维DP 骰子点数
股票买卖系列
子数组最大和 子数组最大和问题

https://zhuanlan.zhihu.com/p/410411231

等差递减区间的个数【转斐波那契】

链表,作为一种最简单的数据组织方式,在平常工作中用处非常广泛。

关于链表,你一定要知道的

链表的常见技巧

  1. 链表中的哨兵节点
  2. 快慢指针
  3. 基于递归的反转问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//单链表 —— 模板题 AcWing 826. 单链表
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;

// 初始化
void init()
{
head = -1;
idx = 0;
}

// 在链表头插入一个数a
void insert(int a)
{
e[idx] = a, ne[idx] = head, head = idx ++ ;
}

// 将头结点删除,需要保证头结点存在
void remove()
{
head = ne[head];
}

//双链表 —— 模板题 AcWing 827. 双链表
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}

// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}

链表相关的经典题目

题目分类 题目名称 考察点 其他说明
删除节点 原地操作,该题并无普遍性
复杂链表的复制 启发式思考,该题并无普遍性
链表归并 归并排序

数组,作为一种最简单的数据组织方式,在平常工作中用处非常广泛。

关于数组你一定要知道的

数组的常见技巧

  • 自Hash

本质:把数组当作hash

给出的数组中每个元素的范围都是 [1, n],并且要求的空间复杂度是$O(1)$

可以考虑:原地修改数组又叫自Hash,其本质上就是把数组当作hash

修改方法:取反,总之需要有办法来识别打了标记的元素

对元素打标记时,打的标记需尽量保存信息(对相反数取绝对值,就能够还原原始内容),不可影响问题的求解。

  • 原地交换

还有另外一种技巧,我们称之为原地交换,其基本要求在于:

每一个元素都需要有对应的坑位,如果我们在他对应的坑位上的话,就进行swap;如此往复

1)检查某一个元素是否在他期望出现的位置上

2)如果在了,就啥也不做

3)如果不在,就与目标位置上的元素进行替换(替换之后,新的元素又不满足了,那我们就继续)

数组相关的经典题目

题目分类 题目名称 考察点 其他说明
缺失的第一个正数 原地交换
数组中重复的数据 原地交换)
数组中重复的数据-无额外空间 二分查找

Pattern: Cyclic Sort 循环排序

参考:
https://blog.csdn.net/weixin_43989102/article/details/111397072

bloom Filter 布隆过滤器
K个hash函数,当写入一个key的时候,设置H1(key), H2(key)…Hk(key)为1。当需要check key是否存在的时候,依次检测H1(key)…Hk(key)是否均为1,如果其中一个不为1,则表示key不存在;否则认为key存在。

注意:有错误判断的可能性,所以仅仅适用于允许一定错误率的场景。

应用:

  1. 爬虫程序url是否处理过
  2. 黑名单,垃圾邮件过滤
  3. 是否关注、判断用户关系(如果出现误判,顶多会多回源一次数据库)
  4. 【TODO,没看明白】Bloom filter在重复数据删除中的应用。主流的重复数据删除技术的基本原理是对文件进行定长或变长分块,然后利用hash函数计算数据块指纹,如果两个数据块指纹相同则认为是重复数据块(同样这里存在数据碰撞问题),只保存一个数据块副本即可,其他相同数据块使用该副本索引号表示,从而实现数据缩减存储空间并提高存储效率。

hash实现

开放定址法:适合数据量小,装载因子小的场景

拉链法:适合存储大对象、大数据量的散列表,更加灵活,有更多的优化策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
一般哈希 —— 模板题 AcWing 840. 模拟散列表
(1) 拉链法
int h[N], e[N], ne[N], idx;

// 向哈希表中插入一个数
void insert(int x)
{
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++ ;
}

// 在哈希表中查询某个数是否存在
bool find(int x)
{
int k = (x % N + N) % N;
for (int i = h[k]; i != -1; i = ne[i])
if (e[i] == x)
return true;

return false;
}

(2) 开放寻址法
int h[N];

// 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
int find(int x)
{
int t = (x % N + N) % N;
while (h[t] != null && h[t] != x)
{
t ++ ;
if (t == N) t = 0;
}
return t;
}

hash使用场景

  • RPC负载均衡
  • 数据分片,分布式存储,例如一致性hash

特殊hash

  1. 自hash, 用数组自身作为hash的container;
  2. 位图
  3. 布隆过滤器,多hash
  4. 地理坐标hash, geo hash,二维转一维。

相关题目

10亿url按照计数排序

思路:

hash统计次数,然后单机排序、或者多机器归并排序

找出字符数组中只出现三次,且最早出现完三次的字符(eg:aabcbba输出b)

思路:

hashmap

数据结构实现

Hashmap是用hash算法实现的,我们通过put一个key和value进行存储,用get(key)来获取,在传入key时,hashmap会根据key.hashCode()计算出hash值存储到bucket里面,当发生hash值相同,也就是hash冲突时,会使用链表+红黑树存储相同的hash值的value,如果冲突少,就用链表,冲突多,就用红黑树

Hashmap扩容:

·HashMap 初始化大小是 16 ,扩容因子默认0.75(可以指定初始化大小,和扩容因子)
·存的对象负载因子0.75(默认)大于总量就要扩容,扩容是键值重新排布,重新对底层数组容量取余分布
·JDK1.8之前是数组+链表结构、JDK1.8及之后是数组+链表+红黑树
·HashMap集合可以存储null键和null值

java数据结构对比

hashMap 数组 + 链表 (冲突链表过长时,转为红黑树)线程不安全 允许null\null 不可以有序遍历

hashTable 线程安全,通过synchronized 不允许null\null 不可以有序遍历

treeMap 红黑树 线程不安全 可以有序遍历

比特位计数 - 题目描述

给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。

比特位计数 - 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
/*
int lowbit(int n) {
return n & (~n);
}
*/
vector<int> countBits(int num) {
vector<int> dp(num + 1);
for (int i = 1; i <= num; ++i) {
dp[i] = dp[i & i - 1] + 1; //每次n&n-1会抹掉最后一个1
}

return dp;
}
};

缺失的第一个正数 - 题目描述

给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数。

缺失的第一个正数 - 总体思路

本地交换法不太容易想到,所以该题目定级为困难。题目让我们找缺失的第一个正数?

我们不妨做一下逆向思考,假设一个正数都不缺失呢?我们会得到什么? 一般情况下,我们的元素下标与元素的值会一一对应,从1到N。

所以,我们考虑尝试着把一个元素i都找到他对应的位置i上,如果位置i被占了,我也强制把位置i的元素移走,重复整个过程。

缺失的第一个正数 - 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
//1~N
//一定可以为i找到一个合适的坑;
for (int i = 1; i <= n; ++i) {
int x = nums[i - 1];
while (x >= 1 && x <= n && x != nums[x - 1]) {
swap(nums[x - 1], nums[i - 1]);
x = nums[i - 1];
}
}

for (int i = 1; i <= n; ++i) {
if (nums[i - 1] != i) {
return i;
}
}

return n + 1;
}
};

逆序第m~n个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* successor;

//非递归实现
ListNode* reverseN(ListNode* head, int n) {
ListNode* pre = NULL;
auto cur = head;
while (cur && n > 0) {
auto next = cur->next; //记录下一个cur
cur->next = pre; //核心步骤---操作cur的next指针,让其指向pre;
pre = cur; //记录前一个
cur = next; //给下一个cur赋值
n--;
}

head->next = cur;
return pre;
}

ListNode* reverseN(ListNode* head, int n) {
if (!head) return head;
//base
if (n == 1) {
successor = head->next; //这一步特别关键,记录当前元素的后继节点,则外面会使用。
return head;
}

//子问题-下一个节点
auto x = reverseN(head->next, n - 1);
//head->next 是递归之前的头,也就是递归之后的尾巴
(head->next)->next = head;
head->next = successor;
return x;
}

ListNode* reverseBetween(ListNode* head, int m, int n) {
//base
if (m == 1) {
return reverseN(head, n);
}

auto x = reverseBetween(head->next, m - 1, n - 1);
head->next = x;
return head; //返回的是头
}
};

每K个反转一次,剩余不到k个,不用反转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverse(ListNode* head, ListNode* tail) {
if (head == tail) return head;

auto x = reverse(head->next, tail);
head->next->next = head;
head->next = NULL;
return tail;
}

ListNode* reverseKGroup(ListNode* head, int k) {
if (!head) return head;

auto p = head;
ListNode* pre = NULL;
for (int i = 0; i < k; ++i) {
//base 不足k个
if (!p) return head;
pre = p;
p = p->next;
}

//反转
reverse(head, pre);

auto x = reverseKGroup(p, k);
head->next = x;
return pre;
}
};