双城记:当手续费遇见冷冻期——动态规划下的股票交易艺术

在金融算法的平行宇宙中,存在两座风格迥异的交易之城:
"手续费之城" 中每笔交易需缴纳过路费,但允许即时折返;
"冷冻期之城" 交易免费,卖出后却被强制冷却一天。
今天,我们将用状态机理论决策优化方程,解开这两座城市的财富密码。跟随动态规划的灯塔,穿透K线迷雾,直抵收益最大化核心!

第一幕:手续费之城的财富迷宫

给定一个整数 n,要求生成所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同二叉搜索树(BST)。二叉搜索树的定义是:对于树中的每一个节点,其左子树中的所有节点值都小于当前节点值,右子树中的所有节点值都大于当前节点值。

▌ 问题本质剖析
题目构建了一个连续决策空间

  • 交易成本结构:每笔卖出操作征收固定费用 fee(沉没成本)

  • 操作约束:空仓→买入→持仓→卖出→空仓(马尔可夫链

  • 目标函数:max(∑(卖出价-买入价-fee))(带约束的优化问题)

▌ 动态规划状态机建模

动态规划和分治法的结合可以帮助我们生成所有可能的二叉搜索树。具体来说,我们可以使用动态规划来记录不同节点数的二叉搜索树结构,并通过分治法递归地生成左右子树。
定义二维状态矩阵 dp[i][s]

  • i ∈ [0, n] 表示时间维度

  • s ∈ {0,1} 表示仓位状态(0=空仓,1=持仓)

状态转移方程揭示财富流动:

dp[i][0] = max( dp[i-1][0], dp[i-1][1] + prices[i] - fee )  
          ↑保持空仓       ↑卖出获利(支付手续费)  
dp[i][1] = max( dp[i-1][1], dp[i-1][0] - prices[i] )  
          ↑保持持仓       ↑买入建仓  

详细分析:

  1. 分治法:对于每个可能的根节点值 i,将剩下的节点分成两部分:
    • 左子树:节点值小于 i
    • 右子树:节点值大于 i
  2. 递归生成:对于左子树和右子树,递归地生成所有可能的二叉搜索树结构。
  3. 组合结果:将左子树和右子树的所有可能组合与根节点组合,得到所有可能的二叉搜索树。

▶ 示例推演(prices=[1,3,2,8,4,9], fee=2)

时间 价格 dp[i][0](空仓收益) dp[i][1](持仓成本) 关键操作
0 1 0 -1 买入
1 3 max(0, -1+3-2)=0 max(-1, 0-3)= -1 无操作
2 2 max(0, -1+2-2)=0 max(-1, 0-2)= -1 无操作
3 8 max(0, -1+8-2)=5 max(-1, 0-8)= -1 卖出获利5
4 4 max(5, -1+4-2)=5 max(-1, 5-4)=1 买入(成本1)
5 9 max(5, 1+9-2)=8 max(1, 5-9)=1 卖出获利8

算法洞察: 手续费作为交易摩擦系数,要求每次卖出必须覆盖 (价差-fee)>0 才有效。状态转移通过机会成本比较(持有vs卖出/买入)实现全局最优。


题目程序:

#include    // 标准输入输出头文件
#include   // 标准库头文件(用于动态内存分配)

// 定义二叉树节点结构体
typedef struct TreeNode {
    int val;                   // 节点存储的整数值
    struct TreeNode *left;     // 指向左子节点的指针
    struct TreeNode *right;    // 指向右子节点的指针
} TreeNode;

/**
 * 生成指定数值范围内的所有可能二叉搜索树
 * start: 当前子树节点值范围起始值
 * end: 当前子树节点值范围结束值
 * returnSize: 输出参数,记录生成的树的数量
 * 返回值: 指向生成的树根节点指针数组的指针
 */
TreeNode** generateTreesHelper(int start, int end, int* returnSize) {
    TreeNode** result = NULL;  // 初始化结果指针数组
    *returnSize = 0;           // 初始化返回的树数量为0
    
    // 处理空树情况(起始值大于结束值)
    if (start > end) {
        *returnSize = 1;              // 只有一种可能:空树
        result = (TreeNode**)malloc(sizeof(TreeNode*));  // 分配一个指针的空间
        result[0] = NULL;             // 将空树指针存入数组
        return result;                // 返回结果
    }
    
    // 遍历当前范围内的每个可能的根节点值
    for (int i = start; i <= end; i++) {
        int leftCount, rightCount;    // 分别记录左右子树的数量
        
        // 递归生成左子树(值小于i的所有可能BST)
        TreeNode** leftTrees = generateTreesHelper(start, i - 1, &leftCount);
        // 递归生成右子树(值大于i的所有可能BST)
        TreeNode** rightTrees = generateTreesHelper(i + 1, end, &rightCount);
        
        // 计算当前根节点i可以生成的树数量
        int currentCount = leftCount * rightCount;
        // 扩展结果数组以容纳新生成的树
        result = (TreeNode**)realloc(result, (*returnSize + currentCount) * sizeof(TreeNode*));
        
        // 组合左右子树的所有可能
        for (int j = 0; j < leftCount; j++) {
            for (int k = 0; k < rightCount; k++) {
                // 创建新根节点
                TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));
                root->val = i;                // 设置节点值
                root->left = leftTrees[j];     // 连接左子树
                root->right = rightTrees[k];   // 连接右子树
                
                // 将新树加入结果数组
                result[*returnSize] = root;
                (*returnSize)++;               // 树计数增加
            }
        }
        
        // 释放左右子树数组(不释放树节点本身)
        free(leftTrees);
        free(rightTrees);
    }
    
    return result;  // 返回生成的树数组
}

/**
 * 生成由1~n构成的所有二叉搜索树
 * n: 节点总数
 * returnSize: 输出参数,记录生成的树数量
 * 返回值: 指向树根节点指针数组的指针
 */
TreeNode** generateTrees(int n, int* returnSize) {
    // 处理n=0的特殊情况
    if (n == 0) {
        *returnSize = 0;      // 没有树
        return NULL;          // 返回空指针
    }
    // 调用辅助函数生成1~n的所有BST
    return generateTreesHelper(1, n, returnSize);
}

/**
 * 主函数:程序入口
 * 功能:测试n=3时生成所有BST并打印根节点值
 */
int main() {
    int n = 3;               // 设置节点数
    int treeCount;            // 存储生成的树数量
    // 生成所有BST
    TreeNode** trees = generateTrees(n, &treeCount);
    
    // 打印生成树的数量
    printf("Total %d BSTs for n = %d\n", treeCount, n);
    printf("Root values: [");
    // 遍历所有生成的树
    for (int i = 0; i < treeCount; i++) {
        // 打印当前树的根节点值
        printf("%d", trees[i]->val);
        if (i < treeCount - 1) printf(", ");  // 用逗号分隔
    }
    printf("]\n");
    
    // 释放动态分配的内存
    for (int i = 0; i < treeCount; i++) {
        // 实际应用中需递归释放整棵树(此处简化处理)
        free(trees[i]);  // 仅释放根节点(实际应递归释放所有节点)
    }
    free(trees);  // 释放树指针数组
    
    return 0;  // 程序正常退出
}

输出结果: 双城记:当手续费遇见冷冻期——动态规划下的股票交易艺术_第1张图片


第二幕:冷冻期之城的时空法则

给定一个整数数组 prices,其中 prices[i] 表示第 i 天的股票价格。要求设计一个算法计算出最大利润。在满足以下约束条件下,可以尽可能地完成更多的交易(多次买卖一支股票):

  • 卖出股票后,你无法在第二天买入股票(即冷冻期为1天)。
  • 你不能同时参与多笔交易(必须在再次购买前出售掉之前的股票)。

▌ 规则本质差异
冷冻期引入时间维度约束

  • 卖出后次日禁止买入(强制冷却)

  • 状态空间升维至三态模型(持仓/非冷冻空仓/冷冻空仓)

▌ 三维状态机架构

动态规划可以帮助我们记录每个状态下的最大利润,并逐步找到最优解。具体来说,我们可以定义三种状态:

  1. 持有股票:当前持有股票,可以卖出或继续持有。
  2. 冷冻期:当前处于冷冻期,无法买入股票。
  3. 无股票且不在冷冻期:当前无股票且不在冷冻期,可以买入股票。即

定义 dp[i][s]

  • s=0:持仓(持有股票)

  • s=1:非冷冻空仓(可买入)

  • s=2:冷冻空仓(禁止买入)

状态转移呈现时间耦合性:

dp[i][0] = max( dp[i-1][0], dp[i-1][1] - prices[i] )  
          ↑保持持仓       ↑非冷冻态买入  
dp[i][1] = max( dp[i-1][1], dp[i-1][2] )  
          ↑保持非冷冻     ↑冷冻解除→非冷冻  
dp[i][2] = dp[i-1][0] + prices[i]  
          ↑卖出操作(进入冷冻)  

▶ 冷冻期与手续费的哲学对比

维度 手续费模型 冷冻期模型
约束类型 成本约束 时间约束
状态维度 二维(仓位) 三维(仓位+冷却状态)
决策焦点 交易频率 vs 手续费 交易时机 vs 冷却惩罚
核心挑战 克服交易摩擦 规避决策盲区

详细分析:

  1. 状态定义
    • hold[i]:第 i 天持有股票的最大利润。
    • freeze[i]:第 i 天处于冷冻期的最大利润。
    • cash[i]:第 i 天无股票且不在冷冻期的最大利润。
  2. 状态转移
    • hold[i] = max(hold[i-1], cash[i-1] - prices[i] - fee):持有股票可以通过继续持有或买入股票得到,买入时支付手续费。
    • freeze[i] = hold[i-1] + prices[i] - fee:卖出股票后进入冷冻期,卖出时支付手续费。
    • cash[i] = max(cash[i-1], freeze[i-1]):无股票且不在冷冻期可以通过保持状态或冷冻期结束得到。
  3. 初始化
    • hold[0] = -prices[0] - fee:第1天买入股票并支付手续费。
    • freeze[0] = 0:第1天无法卖出股票。
    • cash[0] = 0:第1天无股票。
  4. 返回结果max(hold[n-1], freeze[n-1], cash[n-1]) 即为最大利润。

验证示例:

  • 示例1:输入 prices = [1,2,3,0,2]fee = 2,输出为3。
    • 对应的交易状态为:买入 -> 卖出 -> 冷冻期 -> 买入 -> 卖出。
  • 示例2:输入 prices = [1],输出为0。
    • 无法进行交易。

题目程序:

#include    // 标准输入输出头文件
#include   // 标准库头文件(用于动态内存分配和数学函数)
#include   // 定义整数类型极限值

// 定义计算最大利润的函数
int maxProfit(int* prices, int pricesSize, int fee) {
    // 若股票价格序列为空,直接返回0利润
    if (pricesSize == 0) {
        return 0;
    }

    // 动态分配三个状态数组(持有股票/冷冻期/可交易现金)
    int* hold = (int*)malloc(pricesSize * sizeof(int));      // 持有股票状态的最大利润
    int* cooldown = (int*)malloc(pricesSize * sizeof(int)); // 冷冻期状态的最大利润
    int* cash = (int*)malloc(pricesSize * sizeof(int));     // 可交易现金状态的最大利润

    // 初始化第0天的状态
    hold[0] = -prices[0];           // 第0天买入股票,利润为负的股价
    cooldown[0] = INT_MIN;          // 第0天不可能卖出,设为最小整数值(负无穷)
    cash[0] = 0;                    // 第0天不操作,现金为0

    // 从第1天开始动态规划计算
    for (int i = 1; i < pricesSize; i++) {
        // 持有股票状态:继续持有前一天股票 或 从可交易现金状态买入股票
        hold[i] = (hold[i - 1] > cash[i - 1] - prices[i]) ? 
                  hold[i - 1] : cash[i - 1] - prices[i];
        
        // 冷冻期状态:前一天持有股票今天卖出(支付手续费)
        cooldown[i] = hold[i - 1] + prices[i] - fee;
        
        // 可交易现金状态:保持前一天现金状态 或 结束冷冻期
        cash[i] = (cash[i - 1] > cooldown[i - 1]) ? 
                  cash[i - 1] : cooldown[i - 1];
    }

    // 计算最终结果:取最后一天现金状态和冷冻期状态的最大值
    int result = (cash[pricesSize - 1] > cooldown[pricesSize - 1]) ? 
                 cash[pricesSize - 1] : cooldown[pricesSize - 1];
    
    // 释放动态分配的内存
    free(hold);
    free(cooldown);
    free(cash);
    
    return result;  // 返回最大利润
}

// 主函数:程序入口
int main() {
    // 测试用例1:示例数据
    int prices1[] = {1, 2, 3, 0, 2};  // 股票价格数组
    int fee1 = 2;                      // 手续费
    int size1 = sizeof(prices1) / sizeof(prices1[0]);  // 数组长度
    // 计算并打印最大利润
    int profit1 = maxProfit(prices1, size1, fee1);
    printf("输入: [1,2,3,0,2], 手续费=2\n输出: %d\n\n", profit1);

    // 测试用例2:单日数据
    int prices2[] = {1};               // 单日股票价格
    int fee2 = 0;                      // 手续费
    int size2 = sizeof(prices2) / sizeof(prices2[0]);  // 数组长度
    // 计算并打印最大利润
    int profit2 = maxProfit(prices2, size2, fee2);
    printf("输入: [1], 手续费=0\n输出: %d\n", profit2);

    return 0;  // 程序正常退出
}

输出结果: 双城记:当手续费遇见冷冻期——动态规划下的股票交易艺术_第2张图片


第三幕:双城记的终极启示

▌ 算法思想升维

  1. 状态压缩智慧

    • 手续费模型可通过滚动变量降维(cash, hold

    • 冷冻期模型因时间耦合必须保留三维状态

  2. 决策阈值理论

    • 手续费模型中存在隐式交易阈值Δprice > fee

    • 冷冻期模型需计算时间机会成本:冷冻期损失的潜在收益

  3. 现实映射启示

    对比维度 二叉搜索树生成 股票交易冷冻期
    问题类型 组合生成问题 最优化问题
    算法核心 分治法与动态规划结合 动态规划与状态转移方程
    复杂度 时间 O(n^2 * Catalan(n)),空间 O(n^2 * Catalan(n)) 时间 O(n),空间 O(n)
    应用场景 数据结构生成 金融优化与股票交易
    优化目标 生成所有可能的二叉搜索树结构 找到股票交易的最大利润

▌ 收益最大化核心公式

MaxProfit = Σ[ (Peak_i - Valley_j) - N×fee ] - T_cool×Opportunity_Cost
           ↑趋势捕捉能力    ↑成本控制力     ↑时间惩罚因子

当K线在时间轴上跳动,算法操盘手们正在状态转移方程中寻找圣杯。手续费与冷冻期如同金融市场的熵增定律——前者增加交易能耗,后者降低时间效率。真正的炼金术不在于预测市场,而在于构建鲁棒的状态机,在约束中舞蹈。

索罗斯的反射理论告诉我们:市场参与者的认知会改变市场本身。而动态规划的伟大在于——它用数学语言证明,最优决策永远建立在对当前状态的清醒认知上。


当手续费与冷冻期同时存在时,状态空间将如何扩张?欢迎在评论区展开量子金融式讨论!

你可能感兴趣的:(代理模式,c语言,职场和发展,开发语言,算法,动态规划,生活)