Hướng dẫn giải của Đại Lý Bán Sữa
Chỉ dùng lời giải này khi không có ý tưởng, và đừng copy-paste code từ lời giải này. Hãy tôn trọng người ra đề và người viết lời giải.
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Để dễ hình dung hơn, với mỗi truy vấn có các tham số ~s,f,c,r~, ta sẽ hiểu là cần xây dựng một dãy ~a_{p_1}, a_{p_2}, a_{p_3}, ... ,a_{p_{r+1}}~, với dãy ~p~ không giảm và ~p_1 = s~ và ~p_{r+1} = f~, sao cho ~val = max (a_{p_i}-a_{p_{i-1}})~ là bé nhất.
Kết quả của truy vấn sẽ chính là ~val*c~. Như vậy kết quả của bài toán sẽ là ~max(val*c)~ với mọi truy vấn.
Subtask 1
- Ta sẽ dùng phương pháp chặt nhị phân kết quả để tìm giá trị ~val~ .
- Với mỗi giá trị ~mid~, ta có thể kiểm tra xem nó có thỏa mãn không bằng cách xuất phát từ ~a_s~, đi đến một vị trí ~i~ sao cho ~i~ lớn nhất, ~i > s~ và ~a_i - a_s \le mid~. Ta tính đây là một lần dừng lại. Sau đó, ta lại xuất phát từ ~i~ và lặp lại hành động tương tự. Nếu ta có thể đi tới được ~f~ mà không cần dừng lại quá ~r~ lần, giá trị ~mid~ sẽ thỏa mãn.
- Như vậy, ta có thể chặt nhị phân để tìm ~val~ nhỏ nhất thỏa mãn.
- Độ phức tạp ~O(n*log)~ cho một truy vấn.
Subtask 3
- Tuy lần này, ~n \le 400~, nhưng ~m~ lại rất lớn, lên tới ~5*10^5~ nên một thuật toán như ~subtask~ ~1~ sẽ không thể chạy.
- Ta cần sử dụng kĩ thuật quy hoạch động cho bài toán này. Gọi ~dp(l,r,k)~ là kết quả tối ưu (giá trị ~val~) cho đoạn ~[l,r]~ nếu ta chỉ được dừng lại ~k~ lần. Như vậy, với mỗi truy vấn ta sẽ lấy kết quả với ~max (c*dp(s,f,r))~.
- Ta có công thức quy hoạch động như sau: ~dp(l,r,k) = min(max(dp(l,x,k-1),a[r]-a[x]))~, với ~x \in [l,r-1]~.
- Như vậy ta sẽ thu về thuật toán có độ phức tạp ~O(n^4)~, chỉ đủ để qua ~subtask~ ~2~.
- Đến đây ta cần tối ưu công thức này như sau:
- Gọi ~optimize(l,r,k) = x~ là vị trí tốt nhất mà ~dp(l,r,k) = max (dp(l,x,k-1),a[r]-a[x])~ đạt ~min~.
- Ta có nhận xét rằng ~optimize(l,r,k) \le optimize(l,r+1,k)~.
- Mặt khác, ta có ~dp(l,r,k) = min (max (dp(l,x,k-1),a[r]-a[x]))~, mà ~dp(l,x,k-1) \le dp (l,x+1,k-1)~, đồng thời ~a[r] - a[x] > a[r] - a[x+1]~ và hàm ~y = max (dp(l,x,k-1),a[r]-a[x])~ sẽ có dạng giảm dần xuống đáy với từng giá trị ~x~ rồi sau khi chạm đáy giá trị sẽ tăng dần. Như vậy, ta chỉ cần tăng ~x~ tới khi nào ~max (dp(l,x,k-1),a[r]-a[x])~ chạm đáy.
- Vậy ta có thể thực hiện thuật toán ~2~ con trỏ để tính được mảng ~optimize~ song song với việc tính mảng ~dp~.
- Từ đó ta thu được thuật toán ~O(n^3)~.
- Tuy vậy, ta cần tối ưu hóa bộ nhớ, dễ thấy ta không cần lưu chiều ~l~ của ~dp(l,r,k)~, đồng thời không cần lưu mảng ~optimize~. Từ đó bộ nhớ giảm xuống còn ~N^2~.
Code:
#include<iostream>
#include<vector>
using namespace std;
const int N = 405;
const int M = 1e5+5;
int d[N][N];
int n,m;
int a[M];
long long res = 0;
struct qr {
int f,c,r;
};
vector<qr> v[N];
namespace sub1 {
int s,f,c,r;
bool check (int mid) {
int res = 0;
int st = s;
for (int i=s; i<=f; i++) {
int j = i;
while (j < f && a[j+1]-a[i] <= mid) {
j++;
}
if (j == i) return false;
i = j-1;
res++;
if (j == f) break;
} res--;
return res <= r;
}
void solve () {
for (int i=1; i<=n; i++) cin >> a[i];
cin >> s >> f >> c >> r;
long long lef = 0, rig = 1e9, res = 1e9;
while (rig >= lef) {
int mid = (rig+lef) >> 1;
if (check(mid)) res = mid, rig = mid - 1;
else lef = mid + 1;
}
cout << 1ll*res*c << '\n';
}
}
void cal () {
for (int l=1; l<=n; l++) {
for (int r=l; r<=n; r++) {
d[r][0] = a[r] - a[l];
}
for (int k=1; k<=n; k++) {
int opt = l;
for (int r=l; r<=n; r++) {
while (opt < r && max (d[opt][k-1],a[r]-a[opt]) >= max (d[opt+1][k-1],a[r]-a[opt+1])) {
opt++;
}
d[r][k] = max (d[opt][k-1],a[r]-a[opt]);
}
}
for (auto val : v[l]) {
int f = val.f, c = val.c, r = val.r;
res = max (res,1ll*d[f][r]*c);
}
}
}
signed main() {
ios_base::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
if (m == 1) {
sub1::solve();
return 0;
}
for (int i=1; i<=n; i++) {
cin >> a[i];
}
for (int i=1; i<=m; i++) {
int s,f,c,r;
cin >> s >> f >> c >> r;
v[s].push_back({f,c,r});
}
cal();
cout << res;
}