在金融算法的平行宇宙中,存在两座风格迥异的交易之城:
"手续费之城" 中每笔交易需缴纳过路费,但允许即时折返;
"冷冻期之城" 交易免费,卖出后却被强制冷却一天。
今天,我们将用状态机理论和决策优化方程,解开这两座城市的财富密码。跟随动态规划的灯塔,穿透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] ) ↑保持持仓 ↑买入建仓
详细分析:
i
,将剩下的节点分成两部分:
i
。i
。▶ 示例推演(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; // 程序正常退出
}
给定一个整数数组 prices
,其中 prices[i]
表示第 i
天的股票价格。要求设计一个算法计算出最大利润。在满足以下约束条件下,可以尽可能地完成更多的交易(多次买卖一支股票):
▌ 规则本质差异
冷冻期引入时间维度约束:
卖出后次日禁止买入(强制冷却)
状态空间升维至三态模型(持仓/非冷冻空仓/冷冻空仓)
▌ 三维状态机架构
动态规划可以帮助我们记录每个状态下的最大利润,并逐步找到最优解。具体来说,我们可以定义三种状态:
定义 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 冷却惩罚 |
核心挑战 | 克服交易摩擦 | 规避决策盲区 |
详细分析:
hold[i]
:第 i
天持有股票的最大利润。freeze[i]
:第 i
天处于冷冻期的最大利润。cash[i]
:第 i
天无股票且不在冷冻期的最大利润。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])
:无股票且不在冷冻期可以通过保持状态或冷冻期结束得到。hold[0] = -prices[0] - fee
:第1天买入股票并支付手续费。freeze[0] = 0
:第1天无法卖出股票。cash[0] = 0
:第1天无股票。max(hold[n-1], freeze[n-1], cash[n-1])
即为最大利润。验证示例:
prices = [1,2,3,0,2]
,fee = 2
,输出为3。
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; // 程序正常退出
}
▌ 算法思想升维
状态压缩智慧
手续费模型可通过滚动变量降维(cash, hold
)
冷冻期模型因时间耦合必须保留三维状态
决策阈值理论
手续费模型中存在隐式交易阈值:Δprice > fee
冷冻期模型需计算时间机会成本:冷冻期损失的潜在收益
现实映射启示
|
▌ 收益最大化核心公式
MaxProfit = Σ[ (Peak_i - Valley_j) - N×fee ] - T_cool×Opportunity_Cost ↑趋势捕捉能力 ↑成本控制力 ↑时间惩罚因子
当K线在时间轴上跳动,算法操盘手们正在状态转移方程中寻找圣杯。手续费与冷冻期如同金融市场的熵增定律——前者增加交易能耗,后者降低时间效率。真正的炼金术不在于预测市场,而在于构建鲁棒的状态机,在约束中舞蹈。
索罗斯的反射理论告诉我们:市场参与者的认知会改变市场本身。而动态规划的伟大在于——它用数学语言证明,最优决策永远建立在对当前状态的清醒认知上。
当手续费与冷冻期同时存在时,状态空间将如何扩张?欢迎在评论区展开量子金融式讨论!