|
| 1 | +# [Problem 3562: Maximum Profit from Trading Stocks with Discounts](https://leetcode.com/problems/maximum-profit-from-trading-stocks-with-discounts/description/?envType=daily-question) |
| 2 | + |
| 3 | +## Initial thoughts (stream-of-consciousness) |
| 4 | +This is a tree + knapsack problem. We have a company hierarchy (a rooted tree with node 1 as root), and each node can be bought at one of two possible prices depending on whether its direct boss bought (discounted floor(present/2)) or not (full present). Buying a node also affects only its direct children (they get discount if this node bought). We have a total budget (cost limit) and cannot reuse future profit to buy more, so this is a 0/1 choice per node with a global cost constraint. |
| 5 | + |
| 6 | +That suggests a tree DP where for each node we compute, for each possible cost up to budget, the maximum profit achievable in that subtree. Because whether a node is bought affects only its children (not siblings or ancestors beyond parent), we should maintain DP arrays for two contexts: when the parent did buy (so this node would have discount if bought) and when the parent did not buy. |
| 7 | + |
| 8 | +We must combine children contributions like knapsacks (convolution / merging costs). n and budget are ≤ 160, so O(n * budget^2) DP is acceptable. |
| 9 | + |
| 10 | +## Refining the problem, round 2 thoughts |
| 11 | +Plan: |
| 12 | +- Build children adjacency from the hierarchy input (it's already a tree, root=1). |
| 13 | +- For each node u, compute two arrays: |
| 14 | + - dp0[c]: max profit in subtree u with total cost c when u's parent did NOT buy (so if u buys it pays full price). |
| 15 | + - dp1[c]: max profit in subtree u with total cost c when u's parent DID buy (so if u buys it pays discounted floor(present[u]/2)). |
| 16 | +- To compute these, first merge children contributions in two ways: |
| 17 | + - base_no: result of merging child dp0 arrays (children see parent not bought). |
| 18 | + - base_yes: result of merging child dp1 arrays (children see parent bought). |
| 19 | +- Then for dp0: we can either not buy u (use base_no unchanged) or buy u paying full price, gaining future-present, while children behave as if parent bought (use base_yes + node's profit with full price shifted by price). |
| 20 | +- For dp1: similarly, either not buy u (use base_no), or buy u paying discounted price and children use base_yes. |
| 21 | +- Use -inf for impossible entries. Final answer is max dp0[c] for c <= budget at root (root has no parent so parent not bought). |
| 22 | +Edge cases: negative profit (future < price) — algorithm will naturally avoid buying those since not buying is considered. |
| 23 | + |
| 24 | +Complexity: merging children with O(budget^2) per edge gives O(n * budget^2) time, space O(budget) per node during recursion (overall O(n * budget) if persistent). |
| 25 | + |
| 26 | +## Attempted solution(s) |
| 27 | +```python |
| 28 | +from typing import List, Tuple |
| 29 | +import sys |
| 30 | +sys.setrecursionlimit(10000) |
| 31 | + |
| 32 | +def maximumProfit(n: int, present: List[int], future: List[int], hierarchy: List[List[int]], budget: int) -> int: |
| 33 | + # build children adjacency |
| 34 | + children = [[] for _ in range(n + 1)] |
| 35 | + for u, v in hierarchy: |
| 36 | + children[u].append(v) |
| 37 | + |
| 38 | + B = budget |
| 39 | + NEG_INF = -10**9 |
| 40 | + |
| 41 | + def dfs(u: int) -> Tuple[List[int], List[int]]: |
| 42 | + # base_no: children merged when this node is NOT bought (children see parent not bought => use child's dp0) |
| 43 | + # base_yes: children merged when this node IS bought (children see parent bought => use child's dp1) |
| 44 | + base_no = [NEG_INF] * (B + 1) |
| 45 | + base_no[0] = 0 |
| 46 | + base_yes = [NEG_INF] * (B + 1) |
| 47 | + base_yes[0] = 0 |
| 48 | + |
| 49 | + for v in children[u]: |
| 50 | + child_dp0, child_dp1 = dfs(v) |
| 51 | + |
| 52 | + # merge for base_no with child_dp0 |
| 53 | + new_no = [NEG_INF] * (B + 1) |
| 54 | + for c1 in range(B + 1): |
| 55 | + if base_no[c1] == NEG_INF: |
| 56 | + continue |
| 57 | + # c2 cost from child |
| 58 | + # iterate up to remaining budget |
| 59 | + rem = B - c1 |
| 60 | + # typical small loops: iterate c2 and check child's value |
| 61 | + for c2 in range(rem + 1): |
| 62 | + val = child_dp0[c2] |
| 63 | + if val == NEG_INF: |
| 64 | + continue |
| 65 | + nc = c1 + c2 |
| 66 | + cur = base_no[c1] + val |
| 67 | + if cur > new_no[nc]: |
| 68 | + new_no[nc] = cur |
| 69 | + base_no = new_no |
| 70 | + |
| 71 | + # merge for base_yes with child_dp1 |
| 72 | + new_yes = [NEG_INF] * (B + 1) |
| 73 | + for c1 in range(B + 1): |
| 74 | + if base_yes[c1] == NEG_INF: |
| 75 | + continue |
| 76 | + rem = B - c1 |
| 77 | + for c2 in range(rem + 1): |
| 78 | + val = child_dp1[c2] |
| 79 | + if val == NEG_INF: |
| 80 | + continue |
| 81 | + nc = c1 + c2 |
| 82 | + cur = base_yes[c1] + val |
| 83 | + if cur > new_yes[nc]: |
| 84 | + new_yes[nc] = cur |
| 85 | + base_yes = new_yes |
| 86 | + |
| 87 | + # Now build dp0 and dp1 for node u |
| 88 | + dp0 = [NEG_INF] * (B + 1) # parent not bought |
| 89 | + dp1 = [NEG_INF] * (B + 1) # parent bought |
| 90 | + |
| 91 | + # option: node NOT bought -> children merged under base_no, same for dp0 and dp1 |
| 92 | + for c in range(B + 1): |
| 93 | + if base_no[c] != NEG_INF: |
| 94 | + if base_no[c] > dp0[c]: |
| 95 | + dp0[c] = base_no[c] |
| 96 | + if base_no[c] > dp1[c]: |
| 97 | + dp1[c] = base_no[c] |
| 98 | + |
| 99 | + price_full = present[u - 1] |
| 100 | + price_disc = present[u - 1] // 2 |
| 101 | + profit_full = future[u - 1] - price_full |
| 102 | + profit_disc = future[u - 1] - price_disc |
| 103 | + |
| 104 | + # option: node IS bought -> children merged under base_yes, then add node cost & profit |
| 105 | + for c in range(B + 1): |
| 106 | + if base_yes[c] == NEG_INF: |
| 107 | + continue |
| 108 | + # buy when parent not bought: cost add price_full, profit add profit_full => affects dp0 |
| 109 | + nc = c + price_full |
| 110 | + if nc <= B: |
| 111 | + val = base_yes[c] + profit_full |
| 112 | + if val > dp0[nc]: |
| 113 | + dp0[nc] = val |
| 114 | + # buy when parent bought: cost add price_disc, profit add profit_disc => affects dp1 |
| 115 | + nc2 = c + price_disc |
| 116 | + if nc2 <= B: |
| 117 | + val2 = base_yes[c] + profit_disc |
| 118 | + if val2 > dp1[nc2]: |
| 119 | + dp1[nc2] = val2 |
| 120 | + |
| 121 | + return dp0, dp1 |
| 122 | + |
| 123 | + root_dp0, _ = dfs(1) |
| 124 | + # answer is max profit achievable with cost <= budget when root's parent is considered not bought |
| 125 | + ans = max(root_dp0) |
| 126 | + return max(ans, 0) # profit can't be negative, but return 0 if all negative |
| 127 | + |
| 128 | +# The LeetCode function signature would be similar to: |
| 129 | +# class Solution: |
| 130 | +# def maxProfit(self, n: int, present: List[int], future: List[int], |
| 131 | +# hierarchy: List[List[int]], budget: int) -> int: |
| 132 | +# return maximumProfit(n, present, future, hierarchy, budget) |
| 133 | +``` |
| 134 | + |
| 135 | +- Notes about the solution: |
| 136 | + - We perform a DFS on the rooted tree and maintain two knapsack DP arrays per node: dp0 (parent not bought) and dp1 (parent bought). |
| 137 | + - For each node we first merge children contributions under two contexts (child sees parent not bought vs parent bought). Merging is a standard knapsack convolution (double loop up to budget). |
| 138 | + - After merging children, we consider not buying the node (just keep merged children) or buying it (pay price, add profit, and children should have been merged under the "node bought" context). |
| 139 | + - Complexity: Time O(n * budget^2) in the worst case because for each edge we do a merging that is O(budget^2). With n, budget ≤ 160 this is fine (~ a few million operations). Space O(budget) per recursion level (total stack depth <= n). |
| 140 | + - Implementation detail: we use NEG_INF to represent unreachable cost states; initial base lists have base[0] = 0. The final answer is the maximum dp0[c] for c ≤ budget at the root (root has no parent, so parent's bought flag is False). We clamp to at least 0 if desired (problem expects nonnegative profit maximum). |
0 commit comments