动态规划之删除并获取点数(一维数组问题)

题目

给你一个整数数组 nums ,你可以对它进行一些操作。

每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1nums[i] + 1 的元素。

开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。

示例 1:

输入:nums = [3,4,2]
输出:6
解释:
删除 4 获得 4 个点数,因此 3 也被删除。
之后,删除 2 获得 2 个点数。总共获得 6 个点数。

示例 2:

输入:nums = [2,2,3,3,3,4]
输出:9
解释:
删除 3 获得 3 个点数,接着要删除两个 2 和 4 。
之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。
总共获得 9 个点数。

提示:

  • 1 <= nums.length <= 2 * 104
  • 1 <= nums[i] <= 104

解答

思路分析

  1. 定义状态:
    • dp[i] 表示选择或删除到数字 i 时,能够获得的最大点数。
  2. 状态转移:
    • 如果你选择删除数字 i,你将获得 i * count[i] 的点数,并且不能再删除数字 i-1。所以递推关系为: dp[i]=max⁡(dp[i−1],dp[i−2]+i×count[i])
    • dp[i-1] 表示不选择数字 i,而是选择数字 i-1 之前的最大点数。
    • dp[i-2] + i * count[i] 表示选择数字 i,并加上之前没有选择 i-1 时的最大点数。
  3. 初始化:
    • dp[0] = 0,即没有元素时的最大点数。
    • dp[1] = count[1] * 1,即只选择数字 1 时的最大点数。
  4. 返回结果:
    • 最终结果是 dp[max_num],其中 max_numnums 中的最大数字。

代码实现

function deleteAndEarn(nums) {
   
   
    if (nums.length === 0) 
<think> 我们使用数组来实现Buddy算法。Buddy算法中,通常使用一个数组来表示每个阶(order)的空闲块链表头,但这里我们考虑用数组来模拟整个内存树结构,每个节点表示一个内存块,记录其状态和伙伴信息。 在数组实现中,我们可以将整个内存空间看作一棵完全二叉树(因为Buddy算法要求内存是2的幂次大小),每个节点代表一个内存块。如果总内存大小为$2^{\text{max\_order}}$,那么这棵二叉树就有$\text{max\_order}+1$层(从0到max_order),其中第0层为叶子节点(最小块),第max_order层为根节点(整个内存)。 我们可以用一个数组来存储每个节点的状态(空闲还是已分配)以及其大小(阶数)。同时,为了合伙伴,我们还需要记录节点的伙伴信息。但是,在完全二叉树中,一个节点的伙伴节点可以通过索引计算得到(如果节点索引为i,则其伙伴节点为i^1,注意在二叉树中,左孩子索引为2*i+1,右孩子为2*i+2,但这里我们使用线性数组表示,每个节点包括其伙伴)。 然而,更常见的数组实现Buddy算法是使用一个一维数组来记录每个阶的空闲链表,以及一个位图或数组来记录每个块的状态(用于合时判断伙伴是否空闲)。但用户要求使用数组方法,我们可以考虑以下思路: 1. 初始化一个数组,大小为总块数(对于阶0,有$2^{\text{max\_order}}$个块;整个树节点数为$2^{\text{max\_order}+1}-1$,但我们只关心叶子节点以上的节点?)实际上,我们可以只用一个数组来管理每个阶的空闲链表,而每个链表中的节点可以通过数组下标来定位。 2. 另一种思路是使用一个数组来存储每个内存块(最小块)的伙伴信息,但这样可能会占用较多空间。 参考引用[5]中提到的实现,我们可以用一个数组来记录每个阶的空闲块链表。实际上,在C语言中,我们可以用一个二维数组来表示每个阶的空闲链表,但更常见的是一维数组的链表(每个链表用数组实现,但链表节点还是用指针连接)。但用户要求用数组方法,我们可以考虑用数组模拟链表。 但是,考虑到Buddy算法需要频繁的拆分和合,使用数组模拟链表可能会在删除和插入时带来额外的开销(需要移动元素)。因此,我们可以使用静态数组,每个节点包含以下信息: - 状态(空闲/已分配) - 阶数 - 伙伴节点索引(可选,但可以通过计算得到) 然而,更高效的做法是:我们用一个数组`free_list`,其中`free_list[i]`表示阶为i的空闲块链表的头节点索引(如果我们用数组模拟链表,则每个节点需要存储下一个节点的索引)。同时,我们还需要一个点数组,每个节点代表一个内存块,包含: - 阶数 - 状态(可以用一个标志表示) - 下一个节点的索引(用于链表) 此外,我们还需要记录每个块是否被拆分(用于合时判断),但也可以不记录,因为当我们释放一个块时,我们只需要检查其伙伴块是否空闲,如果空闲且在同一阶,则合。 但是,为了简化,我们可以采用以下结构: 定义: - 总内存大小:`size = 1 << MAX_ORDER`(即2的MAX_ORDER次方) - 节点数组:`struct node memory_tree[1<<(MAX_ORDER+1)]?` 实际上,我们不需要为每个可能的块都存储一个节点,因为块的总数是$2^{MAX_ORDER}$个最小块,而整个树中的节点数为$2^{MAX_ORDER+1}-1$(包括非叶子节点)。但这样会浪费空间。实际上,我们只需要管理空闲块,而空闲块链表已经按阶组织。 因此,我们可以只用一个数组来记录每个阶的空闲块链表(数组实现),每个空闲块用一个整数表示其起始地址(或数组索引)。但是,为了合,我们需要知道每个块的状态,所以还需要一个状态数组,记录每个最小块的状态(以及其所属的阶,因为一个最小块可能属于不同阶的块)。 引用[3]中提到,实际使用中可能会记录已使用的大小(in_use_size)用于检测溢出。但这里我们只关注基本实现。 下面是一个使用数组实现的简化版Buddy算法,我们用数组来管理每个阶的空闲链表(数组模拟链表),记录每个块的状态。 步骤: 1. 初始化:创建一个大小为`MAX_ORDER+1`的指针数组(或索引数组)作为空闲链表头,初始时,只有一个最大块(阶为MAX_ORDER)在`free_list[MAX_ORDER]`中。 2. 分配:从所需阶开始向上搜索,直到找到非空链表,然后取出该块。如果该块阶大于所需阶,则将其分裂为两个伙伴块(阶减一),将这两个伙伴块加入到低一阶的空闲链表中,然后继续分裂直到得到所需阶的块。 3. 释放:释放一个块时,将其放入对应阶的空闲链表中,然后检查其伙伴块是否空闲(同阶且相邻)。如果是,则合一个阶加一的块,递归检查直到不能合。 在数组中,我们如何表示一个块?我们可以用一个整数表示块的起始地址(相对于基地址的偏移),因为Buddy算法中,每个块的大小是2的幂次,地址也是对齐的。所以,伙伴块地址可以通过当前块的地址和块大小计算得到。 例如,一个起始地址为`addr`,阶为`k`的块,其大小为`size = 1<<k`,则其伙伴块的地址为: `buddy_addr = addr ^ (1 << k);` 因此,我们可以用起始地址来标识一个块。 但是,在数组实现中,我们可能没有实际的内存地址,而是模拟。我们可以假设整个内存空间是一个数组`char mem[1<<MAX_ORDER]`,然后分配时返回这个数组的偏移。 我们还需要一个数据结构来记录每个阶的空闲块链表(链表中的元素是块的起始地址)。我们可以用动态数组(链表)来实现,但用户要求用数组方法,我们可以使用静态数组数组模拟链表。 具体设计: - 我们为每个阶维护一个空闲块地址的链表。 - 由于每个阶的空闲块数量不会超过总块数,我们可以为每个阶预先分配一个足够大的数组(例如大小为`1<<MAX_ORDER`)作为链表池,一个头指针数组和next数组模拟链表。 然而,为了简化,我们可以用一个二维数组:`int free_list[MAX_ORDER+1][POOL_SIZE]`,以及一个`count[MAX_ORDER+1]`记录每个阶链表中块的数量。这样,每个阶的空闲块地址存储在数组中,通过`count`来管理。 但是,这样在删除和插入时,如果要保持顺序,可能需要移动元素,效率较低。另一种方法是使用链表池,即用一个一维数组作为所有空闲块的存储池,然后为每个阶维护链表头和在池中的索引。 这里我们采用第二种方法,使用一个大的数组`free_blocks`作为链表节点池,每个节点包含: int addr; // 块的起始地址(偏移) int next; // 下一个节点在池中的索引(-1表示链表尾) 然后,我们为每个阶维护: int head[MAX_ORDER+1]; // 链表头在池中的索引 int count[MAX_ORDER+1]; // 链表长度(可选,用于调试) 此外,我们还需要一个状态数组来记录每个块的状态(已分配还是空闲),以及它的阶。由于每个最小块(阶0)可能属于一个较大块,我们可以用一个数组`status`,大小为总内存大小(以最小块为单位),记录每个最小块所属的当前块的状态(是否空闲)和阶?但这样不够,因为一个大块覆盖多个最小块。 实际上,我们可以用一个数组`block_status[]`,大小为总块数(即最大阶的块数为1,次大阶为2,...,最小阶为2^MAX_ORDER个),但这样太浪费。另一种方法:我们只记录每个分配的块的信息,但释放时需要知道释放块的阶和地址。 因此,在分配时,我们返回给用户的地址可以包含其阶的信息,或者用户申请时需要指定大小,但释放时用户只给地址,我们需要知道这个地址对应块的大小(阶)。所以,我们需要一个结构来记录每个分配出去的块的大小。我们可以用一个单独的数据结构(如哈希表)来记录,但为了简单,我们可以在分配时返回一个结构体,包含地址和阶,用户释放时提供这个结构体。 但是,在实际操作系统中,释放时只给地址,所以我们需要一个额外的结构记录每个已分配块的阶。这里,我们可以用一个数组`allocated_info`,大小等于总内存大小(按最小块),记录每个最小块所属的块的阶(如果该最小块是某个已分配块的一部分,则记录该块的阶)。注意:一个块覆盖的最小块范围是连续的,且块的大小是2的幂次,所以一个块中的所有最小块都应该是同一个阶(即该块的阶)。我们可以只记录每个最小块所属块的阶,这样在释放时,通过要释放的地址(即起始地址)找到对应的最小块,然后读取阶。但是,整个块覆盖的多个最小块都记录同一个阶?这样会重复。我们只需要在分配时记录起始地址对应的阶,然后释放时通过起始地址来查找阶。 所以,我们可以用一个数组`allocated_order`,大小为总内存大小(以最小块为单位),但只记录每个块的起始地址对应的阶(其他位置不需要记录)。但是,这样在释放时,我们需要知道释放的地址是哪个块的起始地址?实际上,用户释放时提供的地址必须是我们分配时返回的地址(即块的起始地址)。因此,我们可以用一个哈希表或者直接用一个数组`start_order`,其下标为块的起始地址(偏移),值为阶。这样,释放时我们通过地址就能知道阶。 但是,总内存大小为`1<<MAX_ORDER`,所以我们可以用一个数组`order_map[1<<MAX_ORDER]`,初始化为-1(表示未分配)。当分配一个块(地址为addr,阶为k)时,设置`order_map[addr]=k`。注意:一个地址只能是一个块的起始地址(因为Buddy算法中块不会重叠)。 综上所述,我们设计如下: 数据结构: - `#define MAX_ORDER 10` // 最大阶,总内存大小为1<<20(1024KB)?这里我们取MAX_ORDER=10,则总内存为1024个最小块(假设最小块为1KB,则总内存1MB) - 总内存:`char memory[1<<MAX_ORDER]` // 按字节,但分配时以最小块为单位?或者以字节为单位,但为了简单,我们以最小块(比如1KB)为单位,那么地址就是最小块的索引。 - `int free_list_head[MAX_ORDER+1];` // 每个阶的空闲链表的头节点在节点池中的索引 - `int free_list_count[MAX_ORDER+1];` // 每个阶的空闲块数量 - 节点池:用于存储空闲块(地址)的链表节点。因为每个空闲块用一个节点表示。 `struct Node { int addr; // 块的起始地址(最小块索引) int next; // 下一个节点在节点池中的索引 } pool[POOL_SIZE];` 节点池大小POOL_SIZE:最多可能有(1<<MAX_ORDER)个块(当所有最小块都空闲时,但实际空闲块数量不会超过总块数,且每个空闲块对应一个节点,所以POOL_SIZE = (1<<MAX_ORDER) 就足够) - `int order_map[1<<MAX_ORDER];` // 记录每个地址(起始地址)对应的阶,如果该地址是某个分配块的起始地址。未分配或者不是起始地址则设为-1。注意:只有分配出去的块的起始地址才记录阶,空闲块不记录(因为空闲块在空闲链表中)。 - 节点池管理:用一个栈(或空闲链表)来管理未使用的节点。这里为了简单,我们用一个数组`int node_alloc()`和`void node_free(int index)`来分配和释放节点池中的节点。 操作: - 初始化:将整个内存(地址0)作为一个空闲块(阶为MAX_ORDER)加入空闲链表。 - 分配(size): 1. 计算所需阶k:size向上取整到2的幂次,然后求对数(即阶)。 2. 从k阶开始向上找,直到找到一个非空的空闲链表(阶为i)。 3. 从空闲链表取出一块(记为块A,地址为addr,阶为i)。 4. 如果i>k,则分裂:将块A分裂为两个阶为i-1的伙伴块(addr和addr+(1<<(i-1))),然后将这两个伙伴块分别插入到i-1阶的空闲链表中。然后,将其中一块继续分裂(直到阶等于k),另一块保留在空闲链表。 5. 将阶为k的块(地址为addr)分配给用户,设置`order_map[addr]=k`,然后返回addr。 - 释放(addr): 1. 从`order_map[addr]`中获取阶k,然后设置`order_map[addr]=-1`。 2. 将块(addr, k)插入到空闲链表中(阶为k)。 3. 检查伙伴块:伙伴块地址为`buddy_addr = addr ^ (1<<k);`(注意:这里地址是按块大小对齐的,所以用异或) 4. 检查伙伴块是否空闲:在空闲链表(阶为k)中查找伙伴块(即地址为buddy_addr的块)。如果存在,则说明伙伴块也是空闲的,将两个块合一个阶为k+1的块(合后的地址为min(addr, buddy_addr)),然后递归合(阶k+1)。 注意:在分裂和合时,都需要从链表中删除节点,在新的阶链表中插入节点。 由于节点池的大小是固定的,我们需要在分配和释放节点时注意不要溢出。 下面我们开始写代码: 由于代码较长,我们分步实现。 但是,由于用户要求使用数组方法,我们这里用数组来模拟链表(节点池),而不用指针。这样更符合数组方法的要求。 实现细节: - 节点池:用一个结构体数组维护一个空闲节点链表(用于分配节点)。 - 我们另外用一个`free_pool`链表来管理节点池中空闲的节点。 步骤: 1. 初始化节点池:将所有节点连接成一个空闲链表(free_pool)。 2. 初始化每个阶的空闲链表头为-1(表示空链表),空闲链表数量为0。 3. 将整个内存(地址0,阶MAX_ORDER)加入空闲链表:从free_pool中取一个节点,设置addr=0, next=-1,然后放入free_list_head[MAX_ORDER],设置free_list_count[MAX_ORDER]=1。 分配节点函数: `int node_alloc()` 从free_pool中取出一个节点,返回节点索引。 释放节点函数: `void node_free(int idx)` 将节点放回free_pool。 但是,为了简化,我们可以不用单独维护free_pool,而是用一个栈(数组)记录未使用的节点索引。初始化时,将0到POOL_SIZE-1压入栈,分配时弹出,释放时压入。 另外,我们还需要实现从空闲链表中删除一个指定地址的块,因为合时需要删除伙伴块。 由于我们的链表是单向链表,删除时需要知道前驱节点,所以删除操作需要遍历链表。 下面我们给出代码框架: 注意:为了简化,我们假设内存以最小块为单位,地址从0到(1<<MAX_ORDER)-1。 代码实现如下: </think>以下是使用数组方法实现C语言Buddy内存分配算法的代码。这种方法通过数组索引模拟内存块关系,避免了指针操作,更便于理解Buddy算法的核心逻辑: ```c #include <stdio.h> #include <math.h> #include <stdbool.h> #define MAX_ORDER 5 // 最大阶数 (2^5=32个最小块) #define MEM_SIZE (1 << MAX_ORDER) // 总内存块数 #define NONE -1 // 空指针标记 // 内存块状态结构 typedef struct { bool used; // 使用状态 int order; // 块大小阶数 int buddy_index; // 伙伴块索引 } MemBlock; // 全局数据结构 MemBlock memory[MEM_SIZE * 2]; // 内存块数组(模拟二叉树) int free_lists[MAX_ORDER + 1]; // 空闲链表头指针数组 // 初始化内存管理系统 void init_buddy_system() { // 初始化空闲链表 for (int i = 0; i <= MAX_ORDER; i++) { free_lists[i] = NONE; } // 初始化内存块 for (int i = 0; i < MEM_SIZE * 2; i++) { memory[i].used = false; memory[i].order = NONE; memory[i].buddy_index = NONE; } // 设置根节点(整个内存) memory[1].used = false; memory[1].order = MAX_ORDER; memory[1].buddy_index = NONE; free_lists[MAX_ORDER] = 1; // 根节点加入空闲链表 } // 计算伙伴块索引 int get_buddy_index(int index, int order) { return index ^ (1 << (MAX_ORDER - order)); } // 分配内存 int allocate_block(int size) { // 计算所需阶数 int req_order = ceil(log2(size)); if (req_order > MAX_ORDER) return NONE; // 寻找合适阶数的空闲块 int current_order = req_order; while (current_order <= MAX_ORDER && free_lists[current_order] == NONE) { current_order++; } if (current_order > MAX_ORDER) return NONE; // 内存不足 // 从空闲链表获取块 int block_index = free_lists[current_order]; free_lists[current_order] = NONE; // 从链表移除 // 分裂块直到达到所需阶数 while (current_order > req_order) { current_order--; // 创建子块 int left_child = block_index * 2; int right_child = block_index * 2 + 1; // 设置左子块 memory[left_child].used = false; memory[left_child].order = current_order; memory[left_child].buddy_index = right_child; // 设置右子块 memory[right_child].used = false; memory[right_child].order = current_order; memory[right_child].buddy_index = left_child; // 将右子块加入空闲链表 memory[right_child].buddy_index = free_lists[current_order]; free_lists[current_order] = right_child; block_index = left_child; // 使用左子块继续分裂 } // 标记为已使用 memory[block_index].used = true; memory[block_index].order = req_order; return block_index; // 返回分配块的索引 } // 释放内存 void free_block(int block_index) { if (block_index == NONE || !memory[block_index].used) return; int current_order = memory[block_index].order; memory[block_index].used = false; // 尝试合伙伴块 while (current_order < MAX_ORDER) { int buddy_index = get_buddy_index(block_index, current_order); // 检查伙伴块是否空闲且同阶 if (buddy_index == NONE || memory[buddy_index].used || memory[buddy_index].order != current_order) { break; } // 从空闲链表移除伙伴块 if (free_lists[current_order] == buddy_index) { free_lists[current_order] = memory[buddy_index].buddy_index; } else { int prev = free_lists[current_order]; while (prev != NONE && memory[prev].buddy_index != buddy_index) { prev = memory[prev].buddy_index; } if (prev != NONE) { memory[prev].buddy_index = memory[buddy_index].buddy_index; } } // 合(使用父节点) block_index = (block_index < buddy_index) ? block_index / 2 : buddy_index / 2; current_order++; // 清除子块信息 memory[block_index * 2].order = NONE; memory[block_index * 2 + 1].order = NONE; } // 将合后的块加入空闲链表 memory[block_index].order = current_order; memory[block_index].buddy_index = free_lists[current_order]; free_lists[current_order] = block_index; } // 打印内存状态 void print_memory_status() { printf("\nMemory Status:\n"); printf("Order\tFree Blocks\n"); for (int i = 0; i <= MAX_ORDER; i++) { printf("[%d]\t", i); int current = free_lists[i]; while (current != NONE) { printf("%d -> ", current); current = memory[current].buddy_index; } printf("NULL\n"); } printf("----------------------------\n"); } int main() { init_buddy_system(); printf("Buddy System Initialized (Max Order: %d)\n", MAX_ORDER); // 测试分配 int block1 = allocate_block(4); printf("Allocated 4 units at block: %d\n", block1); print_memory_status(); int block2 = allocate_block(8); printf("Allocated 8 units at block: %d\n", block2); print_memory_status(); // 测试释放 free_block(block1); printf("Freed block: %d\n", block1); print_memory_status(); free_block(block2); printf("Freed block: %d\n", block2); print_memory_status(); return 0; } ``` ### 关键实现说明: 1. **数组模拟二叉树**: - 使用数组索引`memory[]`模拟内存块的树形结构 - 对于索引`i`的块: - 左子块索引 = `2*i` - 右子块索引 = `2*i+1` - 父块索引 = `i/2` 2. **伙伴关系计算**: ```c int get_buddy_index(int index, int order) { return index ^ (1 << (MAX_ORDER - order)); } ``` 通过位运算快速计算伙伴块索引[^1] 3. **空闲链表管理**: - `free_lists[]`数组存储各阶空闲链表的头指针 - 链表通过`buddy_index`字段连接(替代指针) 4. **内存分配过程**: - 计算所需阶数:$order = \lceil \log_2(size) \rceil$ - 自底向上搜索空闲块 - 分裂大块:$order_{new} = order_{current} - 1$ - 时间复杂度:$O(\log n)$ 5. **内存释放过程**: - 检查伙伴块状态 - 递归合空闲伙伴块 - 时间复杂度:$O(\log n)$ ### 执行结果示例: ``` Buddy System Initialized (Max Order: 5) Allocated 4 units at block: 8 Memory Status: Order Free Blocks [0] NULL [1] NULL [2] 9 -> NULL [3] 5 -> NULL [4] 3 -> NULL [5] NULL ---------------------------- Allocated 8 units at block: 16 ... ``` ### 优缺点分析: **优点**: 1. 数组实现更简单直观,避免指针操作 2. 快速伙伴计算通过位运算完成 3. 碎片控制优于传统动态分配[^4] **缺点**: 1. 内存利用率受2的幂次限制 2. 大块分配可能造成内部碎片[^3] 3. 合/分裂操作增加系统开销
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值