Accelerated 01 Knapsack Algorithm and the Quadrangle Inequality
Reference:
Capacitated Dynamic Programming: Faster Knapsack and Graph Algorithms https://arxiv.org/abs/1802.06440
SMAWK Algorithm https://noshi91.github.io/Library/algorithm/smawk.cpp
LARSCH Algorithm https://noshi91.github.io/Library/algorithm/larsch.cpp
I was revisiting my personal statement and I read Professor Kyriakos Axiotis and Professor Christos Tzamos's paper again. I implemented their ideas on the monotonicity of the knapsack problem and record this as a notes of what I learned. Basically, the SMAWK algorithm and the LARSCH algorithm provides an alternative approach for the traditional binary search method for dynamic programming problems that involved the quadrangle inequality.
Quadrangle Inequality
Reference:
Quadrangle Inequality trick for dynamic programs https://tryalgo.org/en/graphs/2022/11/03/optimal-search-tree/
Quadrangle Inequality Properties https://codeforces.com/blog/entry/86306
The function \(w(x,y)\) satisfies quadrangle Inequality if: \[ a\leq b \leq c \leq d \Longrightarrow w(a,c)+w(b,d) \leq w(a,d)+w(b,c) \] If the function \(w(x,y)\) is considered as a matrix \(A_{n,m}\), then it could also been said that \(A_{n,m}\) is Monge. In competitive programming, the Monge matrix is less known and the name "quadrangle inequality" is more often used (at least in where I study). In my opinion they are the same thing but with different names.
To decide whether \(w(x,y)\) satisfies the quadrangle inequality, one only have to decide if \[ w(j,i)+w(j+1,i+1) \leq w(j+1,i)+w(j,i+1) \] Which can be proved by induction.
The optimization of dynamic programming quadrangle inequality is a method discovered by Knuth and sometimes known as Knuth's Optimization. Knuth's Optimization solves:
Case (3) \[ f(i,j) = \min_k(f(i,k)+f(k+1,j)+w(j,i)) \]
Case (4) \[ f(i) = \min_j(w(j,i)) \]
Case (5) \[ f(i) = \min_j (f(j)+w(j,i)) \]
Case (6) \[ f(i) = \min_j(g(j)+w(j,i)) \]
Note: (3) requires \(w(b,c) \leq w(a,d)\). as well as the quadrangle inequality. However in most cases this is easy.
The original method in Knuth, D. E., Optimum binary search trees, Acta Informatica, 1(1), pages 14–25, 1971. Seems only account for case (3). However studying this optimization more we can realize that (4) is the generalized idea for the optimization, and every other case can be converted to case (4). I don't have 39,95 € so I don't really know what is inside this link.
The Optimization
For dynamic programming \(f\) \[ f(i) = \min_{j}(w(j,i)) \] Define \(\text{opt}(i)\) as the largest integer where \[ f(i) = w(\text{opt}(i),i) \]
Theorem 1 (monotone theorem?) (I actually don't know the name) \[ i_1 < i_2 \Longrightarrow \text{opt}(i_1) < \text{opt}(i_2) \] Proof
Suppose for some \(i_1< i_2\), \(j_1 = \text{opt}(i_1) \geq \text{opt}(i_2) = j_2\). Then \(j_2 \leq j_1 \leq i_1 < i_2\).
Then by the quadrangle inequality, \[ w(j_1,i_2) + w(j_2,i_1) \leq w(j_1,i_1) + w(j_2,i_2) \]
However, \[ w(j_2,i_1) \geq w(\text{opt}(i_1),i_1) = w(j_1,i_1) \]
\[ w(j_1,i_2) > w(\text{opt}(i_2),i_2) = w(j_2,i_2) \]
Which means that: \[ w(j_1,i_2) + w(j_2,i_1) > w(j_1,i_1) + w(j_2,i_2) \] Contradiction. \(\square\)
To obtain other forms of the optimization, just do a suitable substitution. If \(\max\) is to replace \(\min\), define new \(w'(x,y) = -w(x,y)\). And prove can be done similarly. The next section gives an example of the application.
The Algorithm
There are commonly two algorithms to apply Theorem 1 (not SMAWK and LARSCH):
Algorithm 1 Just binary search
- In order to find \(f(l\dots r)\), we use binary search to break this section into two parts, \(f(l \dots mid-1)\) and \(f(mid+1 \dots r)\).
- Why \(f(mid)\) is missing? This is because we can evaluate \(f(mid)\) and \(\text{opt}(mid)\) with brute force.
- After finding \(f(mid)\) and \(\text{opt}(mid)\), apply recursing to solve for the left part and the right part.
The algorithm runs with \(O(n\log n)\) time complexity. This is best for Case (6).
Another way to understand this method is compare it to merge sort. It's the same thing.
1 | //Reference: https://oi-wiki.org/dp/opt/quadrangle/ |
Algorithm 2 Binary search using a queue
For the set of \(i\) such that \(j = \text{opt}(i)\), its clear that this set is a contiguous segment that can be denoted by \([l_j,r_j]\). Therefore, we can use a queue maintain a list of possible \(\text{opt}(i)\) that can be the answer in the future. When adding a new element, do binary search to pop elements from the back.
This method is not the focus of this article as clearly this method is more complicated than the previous one. Its only used when \(f(i)\) has self-dependencies (Case 5) but the knapsack problem is (Case 6).
Here is a Chinese source for this method. https://oi-wiki.org/dp/opt/quadrangle/.
The 01 knapsack Problem
Problem Link https://loj.ac/p/6039
For the 01 knapsack problem, define the value of the objects as \(v_i\), the size of the objects as \(s_i\). Then, sort all objects with the same size in descending order and find the prefix sum for each possible size of objects.
Define the prefix sum array as \(p_i[k]\). Where \(i\) is the \(i\)-th unique smallest element is \(s\) and \(k\) is the number of objects chosen with this size. Now, we can do (max,+) convolutions with the \(p_i\) array.
Define \(f_c(j)\) as the answer considering objects from \(p_{i...c}\) and the sum of sizes being \(j\). Then \[ f_c(i) = \max_{j=0}^{i-jc \geq 0} (f_{c-1}(i-jc)+p_c[j]) \]
Note that \(c\) can be considered as a constant.
Define \[ w(j,i) = p_c[i-j] \] Lemma 1 \(w(j,i)\) follows the quadrangle inequality. \[ w(j,i)+w(j+1,i+1) \leq w(j+1,i)+w(j,i+1) \]
\[ p_c[j-i]+p_c[j-i] \leq p_c[j-i-1] + p_c[j-i+1] \]
\[ p_c[j-i]-p_c[j-i-1]\leq p_c[j-i+1]-p_c[j-i] \]
LHS is the element with rank \(j-i\) and RHS is the element with rank \(j-i+1\). So RHS is bigger as the elements are sorted. \(\square\)
Rewriting (14) will obtain: \[ f_c(i) = \max_{j=0}^{i-jc \geq 0} (f_{c-1}(i-jc)+p_c[j]) \\ \]
\[ f_c(i) = \max_{i-(i-j)c\geq 0}^{j=i} (f_{c-1}(i-(i-j)c)+p_c[i-j]) \\ \]
\[ f_c(i) = \max_{i-(i-j)c\geq 0}^{j=i} (g(j)+w(j,i)) \]
Which returns to Case (6).
Lemma 2 \(w'(j,i) = g(j)+w(j,i)\) follows the quadrangle inequality. \[ w'(j,i)+w'(j+1,i+1) \leq w'(j+1,i)+w'(j,i+1) \]
\[ w(j,i)+g(j)+w(j+1,i+1)+g(j+1) \leq w(j+1,i)+g(j+1)+w(j,i+1)+g(j) \\ \]
\[ w(j,i)+w(j+1,i+1) \leq w(j+1,i)+w(j,i+1) \]
\(\square\)
Case (3),(5),(6) can be done similarly. And of course it's left as exercise for readers.
SMAWK algorithm and LARSCH algorithm
I'm not really sure of the mechanism of these two algorithms yet, but SMAWK solves Case (6) in linear time and LARSCH solves Case (5) in linear time. Therefore, the time complexity of 01 knapsack can be optimized from \(O(nW)\) to \(O(DW\log D)\) to $ O(DW)$. Where \(n\) is number of objects, \(W\) is desired total weight, and \(D\) is number of distinct weight.
Appendix
Code 1 Binary Search Solution
1 |
|
Code 2 LARSCH Algorithm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
using namespace std;
// Larsch Algorithm, zero indexed, lower triangular matrix.
// https://noshi91.github.io/Library/algorithm/larsch.cpp
template <class T>
class larsch
{
struct reduce_row;
struct reduce_col;
struct reduce_row
{
int n;
std::function<T(int, int)> f;
int cur_row;
int state;
std::unique_ptr<reduce_col> rec;
reduce_row(int n_) : n(n_), f(), cur_row(0), state(0), rec()
{
const int m = n / 2;
if (m != 0)
{
rec = std::make_unique<reduce_col>(m);
}
}
void set_f(std::function<T(int, int)> f_)
{
f = f_;
if (rec)
{
rec->set_f([&](int i, int j) -> T
{ return f(2 * i + 1, j); });
}
}
int get_argmin()
{
const int cur_row_ = cur_row;
cur_row += 1;
if (cur_row_ % 2 == 0)
{
const int prev_argmin = state;
const int next_argmin = [&]()
{
if (cur_row_ + 1 == n)
{
return n - 1;
}
else
{
return rec->get_argmin();
}
}();
state = next_argmin;
int ret = prev_argmin;
for (int j = prev_argmin + 1; j <= next_argmin; j += 1)
{
if (f(cur_row_, ret) > f(cur_row_, j))
{
ret = j;
}
}
return ret;
}
else
{
if (f(cur_row_, state) <= f(cur_row_, cur_row_))
{
return state;
}
else
{
return cur_row_;
}
}
}
};
struct reduce_col
{
int n;
std::function<T(int, int)> f;
int cur_row;
std::vector<int> cols;
reduce_row rec;
reduce_col(int n_) : n(n_), f(), cur_row(0), cols(), rec(n) {}
void set_f(std::function<T(int, int)> f_)
{
f = f_;
rec.set_f([&](int i, int j) -> T
{ return f(i, cols[j]); });
}
int get_argmin()
{
const int cur_row_ = cur_row;
cur_row += 1;
const auto cs = [&]() -> std::vector<int>
{
if (cur_row_ == 0)
{
return {{0}};
}
else
{
return {{2 * cur_row_ - 1, 2 * cur_row_}};
}
}();
for (const int j : cs)
{
while ([&]()
{
const int size = cols.size();
return size != cur_row_ && f(size - 1, cols.back()) > f(size - 1, j); }())
{
cols.pop_back();
}
if (cols.size() != n)
{
cols.push_back(j);
}
}
return cols[rec.get_argmin()];
}
};
std::unique_ptr<reduce_row> base;
public:
larsch(int n, std::function<T(int, int)> f)
: base(std::make_unique<reduce_row>(n))
{
base->set_f(f);
}
int get_argmin() { return base->get_argmin(); }
};
const int maxn = 5e4 + 5;
const int maxk = 305;
int n, m, lim;
int sz[maxk], f[maxn], g[maxn], t[maxn];
vector<int> s[maxk], a[maxk];
void solve(int c, int x)
{
const auto w = [&](const int ip, const int jp, const int x, const bool isLarsch) -> int
{
int i = ip;
int j = jp;
if (isLarsch)
i++, j++;
if (j >= i)
return 0;
else
return g[t[j]] + a[x][i - j];
};
larsch<int> opt(c, [&](int i, int j)
{ return -w(i, j, x, true); });
for (int i = 1; i <= c; i++)
{
int j = opt.get_argmin();
if (i == 1)
continue;
f[t[i]] = max(f[t[i]], w(i, j + 1, x, false));
}
}
signed main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
int y;
cin >> y;
lim = max(lim, x);
s[x].push_back(y);
}
for (int i = 1; i <= lim; i++)
{
if (!s[i].size())
continue;
sort(s[i].begin(), s[i].end());
reverse(s[i].begin(), s[i].end());
a[i].resize(m / i + 5);
a[i][0] = 0;
for (int j = 1; j < a[i].size(); j++)
{
if (j - 1 < s[i].size())
a[i][j] += s[i][j - 1];
a[i][j] += a[i][j - 1];
}
for (int j = 0; j <= m; j++)
g[j] = f[j];
for (int j = 0; j < i; j++)
{
int c = 0;
for (int k = j; k <= m; k += i)
t[++c] = k;
if (c >= 2)
solve(c, i);
}
vector<int>().swap(a[i]);
}
for (int i = 1; i <= m; i++)
{
cout << f[i] << " ";
}
}
Code 3 SMAWK Algorithm
1 |
|