Skip to content

Commit 2bddb8d

Browse files
committed
Add best to buy sell
1 parent a276785 commit 2bddb8d

File tree

5 files changed

+230
-0
lines changed

5 files changed

+230
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Thời Điểm Tốt Nhất Để Mua và Bán Cổ Phiếu
2+
3+
## Mô tả công việc
4+
5+
Giả sử bạn có một mảng giá với phần tử thứ `i` là giá cổ phiếu của ngày `i`.
6+
7+
Hãy tìm lợi nhuận lớn nhất có thể. Bạn có thể thực hiện bao nhiêu giao dịch tuỳ thích (có thể mua một cổ phiếu và bán một cổ phiếu nhiều lần)
8+
9+
> Lưu ý: Bạn không thể thực hiện nhiều giao dịch với một cổ phiếu cùng thời điểm (vd, bạn phải bán cổ phiếu trước khi mua lại nó lần nữa).
10+
11+
**Ví dụ 1**
12+
13+
```
14+
Input: [7, 1, 5, 3, 6, 4]
15+
Output: 7
16+
```
17+
18+
_Giải thích_ : Mua vào ngày thứ `2` với giá là `1` và bán nó vào ngày thứ `3` với giá là `5`, lợi nhuận là `4`. Sau đó mua lại và vào ngày thứ `4` với giá là `3` sau đấy bán lại vào ngày thứ `5` với giá là `6`, lợi nhuận có thêm là `3`.
19+
20+
**Ví dụ 2**
21+
22+
```
23+
Input: [1, 2, 3, 4, 5]
24+
Output: 4
25+
```
26+
27+
_Giải thích_: Mua vào ngày đầu tiên (`price - 1`) và bán vào ngày cuối cùng (`price = 5`), lợi nhuận thu được `profit = 5-1 = 4`. Lưu ý bạn không thể mua vào ngày 1, tiếp tục mua vào ngày thứ 2 và bán chúng sau. Vì như vậy là thực hiện nhiều giao dịch, ta phải bán nó trước khi mua lại.
28+
29+
**Ví dụ 3**
30+
31+
```
32+
Input: [7, 6, 4, 3, 1]
33+
Output: 0
34+
```
35+
36+
_Giải thích_: vì giá cổ phiếu giảm dần nên lợi nhuận lớn nhất là không mua gì cả `profit = 0`.
37+
38+
## Giải pháp
39+
40+
### Giải pháp chia để trị `O(2^n)`
41+
42+
Ta sẽ thử **tất cả** các trường hợp mua bán để tìm ra lợi nhuận lớn nhất có thể bằng giải pháp _chia để trị_.
43+
44+
Bắt đầu ta có một mảng giá `[7, 6, 4, 3, 1]` và ta ở vị trị của ngày thứ _nhất_ (đầu mảng) của giao dịch. Tại thời điểm này, ta có thể nói rằng tổng lợi nhuận tối đa sẽ là _giá trị lớn nhất_ của hai giá trị sau:
45+
46+
1. _Giữ lại tiền_ → tổng lợi nhuận sẽ bằng lợi nhuận từ việc mua/bán cổ phiếu còn lại → `keepProfit = profit([6, 4, 3, 1])`.
47+
2. _Mua/bán theo giá hiện tại_ → lợi nhuận trong trường hợp này sẽ bằng lợi nhuận từ việc mua/bán cổ phiếu còn lại cộng (hoặc trừ, tùy thuộc vào việc đang bán hay mua) giá cổ phiếu hiện tại → `buySellProfit = -7 + profit([6, 4, 3, 1])`.
48+
49+
Tổng lợi nhuận sẽ bằng → `overalProfit = Max(keepProfit, buySellProfit)`.
50+
51+
Như vậy, bạn có thể thấy `profit([6, 4, 3, 1])` đang được giải quyết theo cùng một cách đệ quy.
52+
53+
> Xem [dqBestTimeToBuySellStocks.js](dqBestTimeToBuySellStocks.js)
54+
55+
### Độ phức tạp thời gian
56+
57+
Tất cả các gọi đệ quy sẽ tạo ra _2_ nhánh đệ quy. Độ sâu của đệ quy là `n` (độ dài của mảng) do đó, độ phức tạp thời gian sẽ là `O(2^n)`
58+
59+
Như bạn có thể thấy, điều này là rất kém hiệu quả. Ví dụ với mảng giá kích thước `20`, số lượng gọi đệ quy sẽ ở đâu đó gần với `2M`!
60+
61+
### Độ phức tạp không gian bổ sung
62+
63+
Nếu ta tránh sao chép mảng giữa các lần gọi đệ quy và sẽ sử dụng con trỏ mảng thì độ phức tạp của không gian bổ sung sẽ tỷ lệ với độ sâu của đệ quy: `O (n)`
64+
65+
## Giải pháp Peek Valley `O(n)`
66+
67+
Nếu ta phát hoạ mảng giá (vd `[7, 1, 5, 3, 6, 4]`) ta sẽ nhận thấy các điểm đáng lưu ý là các đỉnh và đáy liên tiếp.
68+
69+
![Peak Valley Approach](https://leetcode.com/media/original_images/122_maxprofit_1.PNG)
70+
71+
_Image source: [LeetCode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/solution/)_
72+
73+
Vì vậy, nếu ta theo dõi giá đang tăng và sẽ bán cổ phiếu ngay lập tức _trước_ khi giá đi xuống, ta sẽ thu được lợi nhuận tối đa (hãy nhớ rằng ta đã mua cổ phiếu ở đáy với giá thấp của nó).
74+
75+
> Xem [peakvalleyBestTimeToBuySellStocks.js](peakvalleyBestTimeToBuySellStocks.js)
76+
77+
### Độ phức tạp thời gian
78+
79+
Vì thuật toán chỉ yêu cầu một lần chuyển qua mảng giá nên độ phức tạp về thời gian sẽ bằng `O (n)`.
80+
81+
### Độ phức tạp không gian bổ sung
82+
83+
Ngoại trừ mảng giá, thuật toán sử dụng lượng bộ nhớ không đổi. Do đó, độ phức tạp không gian bổ sung là `O (1)`.
84+
85+
## Giải pháp tích luỹ `O(n)`
86+
87+
Có một cách tiếp cận đơn giản hơn tồn tại. Giả sử ta có mảng giá trông như sau `[1, 7, 2, 3, 6, 7, 6, 7]`:
88+
89+
![Simple One Pass](https://leetcode.com/media/original_images/122_maxprofit_2.PNG)
90+
91+
_Image source: [LeetCode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/solution/)_
92+
93+
Bạn có thể nhận thấy rằng ta thậm chí không cần phải theo dõi mức giá liên tục tăng. Thay vào đó, ta có thể chỉ cần thêm chênh lệch giá cho _tất cả các đoạn đang phát triển_ của biểu đồ mà cuối cùng sẽ tính đến lợi nhuận cao nhất có thể,
94+
95+
> Xem [accumulatorBestTimeToBuySellStocks.js](accumulatorBestTimeToBuySellStocks.js)
96+
97+
98+
### Độ phức tạp về thời gian
99+
100+
Vì thuật toán chỉ yêu cầu một lần chuyển qua mảng giá nên độ phức tạp về thời gian sẽ bằng `O (n)`.
101+
102+
### Độ phức tạp không gian bổ sung
103+
104+
Ngoại trừ mảng giá, thuật toán sử dụng lượng bộ nhớ không đổi. Do đó, độ phức tạp không gian bổ sung là `O (1)`.
105+
106+
## Liên kết
107+
108+
- [Best Time to Buy and Sell Stock on LeetCode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Tìm lợi nhuận lớn nhất từ việc mua bán cổ phiếu bẳng giải pháp
3+
* TÍCH LUỸ.
4+
*
5+
* @param {number[]} prices - Mảng giá cổ phiếu, vd [7, 6, 4, 3, 1]
6+
* @param {function(): void} visit - Truy cập hàm callback để tính toán số lần lặp lại.
7+
* @return {number} - Lợi nhuận lớn nhất
8+
*/
9+
const accumulatorBestTimeToBuySellStocks = (prices, visit = () => { }) => {
10+
visit();
11+
let profit = 0;
12+
for (let day = 1; day < prices.length; day += 1) {
13+
visit();
14+
// Cộng phần tăng giá từ hôm qua đến hôm nay (nếu có) vào lợi nhuận.
15+
profit += Math.max(prices[day] - prices[day - 1], 0);
16+
}
17+
return profit;
18+
};
19+
20+
export default accumulatorBestTimeToBuySellStocks;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Tìm lợi nhuận lớn nhất từ việc mua bán cổ phiếu bẳng giải pháp
3+
* QUY HOẠCH ĐỘNG
4+
*
5+
* @param {number[]} prices - Mảng giá cổ phiếu, vd [7, 6, 4, 3, 1]
6+
* @param {function(): void} visit - Truy cập hàm callback để tính toán số lần lặp lại.
7+
* @return {number} - Lợi nhuận lớn nhất
8+
*/
9+
const dpBestTimeToBuySellStocks = (prices, visit = () => { }) => {
10+
visit();
11+
let lastBuy = -prices[0];
12+
let lastSold = 0;
13+
14+
for (let day = 1; day < prices.length; day += 1) {
15+
visit();
16+
const curBuy = Math.max(lastBuy, lastSold - prices[day]);
17+
const curSold = Math.max(lastSold, lastBuy + prices[day]);
18+
lastBuy = curBuy;
19+
lastSold = curSold;
20+
}
21+
22+
return lastSold;
23+
};
24+
25+
export default dpBestTimeToBuySellStocks;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Tìm lợi nhuận lớn nhất từ việc mua bán cổ phiếu bẳng giải pháp
3+
* CHIA ĐỂ TRỊ.
4+
*
5+
* @param {number[]} prices - Mảng giá cổ phiếu, vd [7, 6, 4, 3, 1]
6+
* @param {function(): void} visit - Truy cập hàm callback để tính toán số lần lặp lại.
7+
* @return {number} - Lợi nhuận lớn nhất
8+
*/
9+
const dqBestTimeToBuySellStocks = (prices, visit = () => { }) => {
10+
/**
11+
* Triển khai đệ quy ở hàm main. Nó được ẩn với người dùng.
12+
*
13+
* @param {boolean} buy - Lựa chọn mua hoặc bán
14+
* @param {number} day - ngày hiện tại của giao dịch (chỉ số hiện tại trong mảng)
15+
* @returns {number} - lợi nhuận lớn nhất từ việc mua/bán
16+
*/
17+
const recursiveBuyerSeller = (buy, day) => {
18+
// Gọi đệ quy để tính toán độ phức tạp.
19+
visit();
20+
21+
// Thoát đệ quy nếu đây là ngày giao dịch cuối cùng (mảng giá đã kết thúc).
22+
if (day === prices.length) {
23+
return 0;
24+
}
25+
26+
// Nếu ta mua - sẽ mất tiền (-1), nếu ta bán - sẽ nhận tiền (+1)
27+
const operationSign = buy ? -1 : +1;
28+
return Math.max(
29+
// Lựa chọn 1: Đừng làm gì.
30+
recursiveBuyerSeller(buy, day + 1),
31+
// Lựa chọn 2: Mua hoặc bán với giá hiên tại.
32+
operationSign * prices[day] + recursiveBuyerSeller(!buy, day + 1),
33+
);
34+
};
35+
36+
const buy = true;
37+
const day = 0;
38+
39+
return recursiveBuyerSeller(buy, day);
40+
};
41+
42+
export default dqBestTimeToBuySellStocks;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Tìm lợi nhuận lớn nhất từ việc mua bán cổ phiếu bẳng giải pháp
3+
* PEAK VALLEY
4+
*
5+
* @param {number[]} prices - Mảng giá cổ phiếu, vd [7, 6, 4, 3, 1]
6+
* @param {function(): void} visit - Truy cập hàm callback để tính toán số lần lặp lại.
7+
* @return {number} - Lợi nhuận lớn nhất
8+
*/
9+
const peakvalleyBestTimeToBuySellStocks = (prices, visit = () => { }) => {
10+
visit();
11+
let profit = 0;
12+
let low = prices[0];
13+
let high = prices[0];
14+
15+
prices.slice(1).forEach((currentPrice) => {
16+
visit();
17+
if (currentPrice < high) {
18+
// Nếu giá đi xuống, ta sẽ mua lại nó.
19+
profit += high - low;
20+
low = currentPrice;
21+
high = currentPrice;
22+
} else {
23+
// Nếu giá tăng, ta không cần làm gì ngoài việc tăng bản ghi.
24+
high = currentPrice;
25+
}
26+
});
27+
28+
// Trong trường hợp nếu giá tăng trong ngày cuối cùng và
29+
// ta không có cơ hội bán trong vòng lặp forEach.
30+
profit += high - low;
31+
32+
return profit;
33+
};
34+
35+
export default peakvalleyBestTimeToBuySellStocks;

0 commit comments

Comments
 (0)