Cho
-
$\bigl|,y_{i+1} - y_i,\bigr| ;\le; D_{\text{max}}$ , với mọi$i = 1, 2, \ldots, n-1$ . - Tối ưu (minimize) tổng sai số có trọng số: $ \sum_{i=1}^{n} w_i \cdot \bigl|,x_i - y_i,\bigr|. $
Nói cách khác, ta cần tìm
- Giả sử
$D_{\text{max}} = 3$ . - Danh sách lá:
- Lá 1:
$x_1 = 44,; w_1 = 1$ - Lá 2:
$x_2 = 4,;;, w_2 = 1$ - Lá 3:
$x_3 = 10,; w_3 = 1000$ - Lá 4:
$x_4 = 4,;;, w_4 = 1$
- Lá 1:
- Một lời giải có thể là $ (y_1, y_2, y_3, y_4) = (16, 13, 10, 7) $.
- Kiểm tra ràng buộc:
$\lvert,y_2 - y_1\rvert = 3 \le 3$ ,$\lvert,y_3 - y_2\rvert = 3 \le 3$ ,$\lvert,y_4 - y_3\rvert = 3 \le 3$ . - Tổng chi phí
$;= w_1\lvert 44 - 16\rvert + w_2\lvert 4 - 13\rvert + w_3\lvert 10 - 10\rvert + w_4\lvert 4 - 7\rvert$ .
- Kiểm tra ràng buộc:
Vì trong bài toán này, có thể tồn tại nhiều nghiệm tối ưu khác nhau (cho chi phí tối ưu như nhau), nên lời giải thu được có thể không duy nhất.
Vì các
-
Giới hạn miền tìm kiếm vào một khoảng
$[\min x - \delta, \max x + \delta]$ . -
Chia khoảng này thành nhiều điểm rời rạc với một bước nhảy
$\text{step}$ . - Tạo mảng
grid
gồm các giá trị rời rạc này, và bắt buộc$y_i$ phải chọn tronggrid
.
- Ký hiệu
$\text{grid}[j]$ là giá trị thứ$j$ trong lưới. - Đặt
$\text{dp}[i][j]$ = chi phí tối ưu khi lá thứ$i$ được đặt ở$\text{grid}[j]$ .
Công thức chuyển trạng thái:
$ \text{dp}[i][j] ;=; \min_{\substack{k:\|\text{grid}[j] - \text{grid}[k]| \le D_{\max}}} \Bigl(\text{dp}[i-1][k]\Bigr) ;+; w_i \cdot \bigl|,x_i - \text{grid}[j],\bigr|. $
- Nghĩa là, để đặt lá
$i$ ở$\text{grid}[j]$ , lá$i-1$ phải ở một vị trí$\text{grid}[k]$ gần$\text{grid}[j]$ trong phạm vi$D_{\max}$ . - Sau khi tìm
$\text{dp}[i][j]$ , ta chọn giá trị nhỏ nhất trong số các$k$ thỏa mãn.
- Để khôi phục được nghiệm
$y_i$ , ta dùng một mảngparent[i][j]
, ghi lại vị trí$k$ (tronggrid
) giúp$\text{dp}[i][j]$ đạt giá trị min. - Sau khi tính xong, ta dò lại
$\text{dp}[n-1][j]$ để tìm chi phí nhỏ nhất (gọi làbest_j
). - Từ
best_j
, lần ngược vềi = n-1, n-2, ..., 0
(dùngparent[i][best_j]
) để tìm ra dãy$y_i$ .
Trong quá trình tìm
Ví dụ, trong đoạn code:
double best_prev = INF;
int best_k = -1;
for (int k = k_left; k <= k_right; k++) {
if (dp_prev[k] < best_prev) {
best_prev = dp_prev[k];
best_k = k;
}
}
- Ở đây, nếu
dp_prev[k] == best_prev
thì ta giữ nguyênbest_k
(không cập nhật). - Kết quả: k đầu tiên (trong khoảng
$[k_{\text{left}}, k_{\text{right}}]$ ) mà ta gặp có chi phí$dp_prev[k] = best_prev$ sẽ được chọn.
Tuy nhiên, nếu muốn khi tie thì chọn
for (int k = k_left; k <= k_right; k++) {
if (dp_prev[k] < best_prev) {
// Tìm được chi phí nhỏ hơn => cập nhật
best_prev = dp_prev[k];
best_k = k;
}
else if (fabs(dp_prev[k] - best_prev) < 1e-12) {
// Chi phí ngang nhau (tie)
// Thay vì giữ nguyên, ta sẽ có tiêu chí phụ:
// 1) Chọn k lớn hơn
if (k > best_k) {
best_k = k;
}
// 2) Hoặc chọn k nhỏ hơn (tùy ý)
// if (k < best_k) {
// best_k = k;
// }
}
}
Việc lựa chọn tie-break này sẽ ảnh hưởng đến dãy
- Tie-break “chọn k đầu tiên”: Kết quả có xu hướng ổn định, vì chọn “vừa” gặp cost min là dừng.
- Tie-break “chọn k lớn nhất”: Khi có nhiều vị trí cùng cost, nghiệm “ngả” về grid có chỉ số (hoặc giá trị) cao hơn.
- Tie-break “chọn k nhỏ nhất”: Ngược lại, nghiệm “ngả” về grid có chỉ số (hoặc giá trị) thấp hơn.
Ta cũng có thể đổi chiều duyệt (từ M-1
đến 0
) thay vì 0
đến M-1
để đạt hiệu ứng tương tự.
Vì bài toán có thể có nhiều nghiệm tối ưu khác nhau (do các lá có thể dịch chuyển trong giới hạn
Dưới đây là ví dụ code C++ hoàn chỉnh. Trong đó, phần tie-break được chú thích rõ ràng, ta có thể điều chỉnh để thay đổi kết quả lời giải.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
// ===== Bước 1: Nhập dữ liệu =====
int n;
double Dmax;
cin >> n >> Dmax;
vector<double> x(n), w(n);
for(int i = 0; i < n; i++) {
cin >> x[i] >> w[i];
}
// ===== Bước 2: Tạo lưới rời rạc (grid) =====
double minX = *min_element(x.begin(), x.end()) - 10.0;
double maxX = *max_element(x.begin(), x.end()) + 10.0;
double step = 1.0; // có thể chỉnh nhỏ hơn để tăng độ chính xác
vector<double> grid;
for (double val = minX; val <= maxX; val += step) {
grid.push_back(val);
}
int M = (int)grid.size();
// ===== Bước 3: Chuẩn bị mảng DP và parent =====
// dp_prev[j] = chi phí tối ưu cho lá (i-1), chọn grid[j]
// dp_curr[j] = chi phí tối ưu cho lá i, chọn grid[j]
// parent[i][j] = chỉ số k giúp dp[i][j] đạt giá trị tối ưu
const double INF = 1e18;
vector<double> dp_prev(M, INF), dp_curr(M, INF);
vector<vector<int>> parent(n, vector<int>(M, -1));
// ===== Bước 4: Khởi tạo cho lá đầu tiên (i = 0) =====
for (int j = 0; j < M; j++) {
dp_prev[j] = w[0] * fabs(x[0] - grid[j]);
parent[0][j] = -1;
}
// ===== Bước 5: Tính DP cho i = 1..n-1 =====
for (int i = 1; i < n; i++) {
for (int j = 0; j < M; j++) {
double yj = grid[j];
// Xác định khoảng [k_left..k_right] sao cho |grid[k] - yj| <= Dmax
double leftVal = yj - Dmax;
double rightVal = yj + Dmax;
int k_left = int(lower_bound(grid.begin(), grid.end(), leftVal) - grid.begin());
int k_right = int(upper_bound(grid.begin(), grid.end(), rightVal) - grid.begin()) - 1;
if (k_left < 0) k_left = 0;
if (k_right >= M) k_right = M - 1;
// Tìm min trong dp_prev[k] (k_left <= k <= k_right)
double best_prev = INF;
int best_k = -1;
for (int k = k_left; k <= k_right; k++) {
// So sánh dp_prev[k] với best_prev
if (dp_prev[k] < best_prev) {
best_prev = dp_prev[k];
best_k = k;
}
else if (fabs(dp_prev[k] - best_prev) < 1e-12) {
// ======== TIE-BREAK VÍ DỤ ========
// Nếu cùng chi phí, chọn k lớn hơn
// (có thể chuyển thành k < best_k hay k random,... tùy ý)
if (k > best_k) {
best_k = k;
}
}
}
// Lúc này, best_prev là dp_prev[k] tốt nhất
dp_curr[j] = best_prev + w[i] * fabs(x[i] - yj);
parent[i][j] = best_k;
}
// Copy dp_curr -> dp_prev
dp_prev = dp_curr;
// Reset dp_curr cho lượt tiếp theo
fill(dp_curr.begin(), dp_curr.end(), INF);
}
// ===== Bước 6: Tìm kết quả tối ưu và truy vết =====
double ans = INF;
int best_j = -1;
for (int j = 0; j < M; j++) {
if (dp_prev[j] < ans) {
ans = dp_prev[j];
best_j = j;
}
}
// Truy vết ngược để khôi phục dãy y
vector<double> y(n);
int curr_j = best_j;
for (int i = n - 1; i >= 0; i--) {
y[i] = grid[curr_j];
curr_j = parent[i][curr_j];
}
// ===== Bước 7: In kết quả =====
cout << fixed << setprecision(6);
cout << "Min cost = " << ans << "\n";
cout << "Sequence y:\n";
for (int i = 0; i < n; i++) {
cout << "y[" << i << "] = " << y[i] << "\n";
}
return 0;
}
Trong đoạn // TIE-BREAK VÍ DỤ
, có thể:
- Chọn k lớn hơn khi tie:
if (k > best_k) best_k = k;
- Chọn k nhỏ hơn khi tie:
if (k < best_k) best_k = k;
- Hoặc thậm chí chọn ngẫu nhiên (random) một trong các k tie.
Việc thay đổi này không làm tăng chi phí tối ưu, mà chỉ thay đổi phân bố