---
tags: 109_FDCS, report
---
<!--
## 複習
#### 疊代版快速冪code
:::spoiler code
```cpp=
constexpr int mod = 998244353;
long long pow(long long a, long long b) {
long long ret = 1;
for (; b; b = b >> 1, a = a * a % mod)
if (b & 1)
ret = ret * a % mod;
return ret;
}
```
:::
#### 遞迴版快速冪code
:::spoiler code
```cpp=
constexpr int mod = 998244353;
long long pow(long long a, long long b) {
if (!a) return;
long long tmp = pow(a, b >> 1);
return b & 1 ? tmp * tmp % mod * a % mod : tmp * tmp % mod;
}
```
:::
#### 費氏數列轉移式
:::spoiler
{$$dp_i=\begin{cases}i&i<2\\dp_{i-1}+dp_{i-2}&otherwise\end{cases}$$}
:::
#### $O(nm)$的LCS DP解滾動後空間複雜度為何?
:::spoiler
$\Theta(min(n,m))$
:::
---
-->
# Greedy & 進階DP
# Greedy
每一步選擇中都採取在當前狀態下最好的選擇
從而**希望**導致結果是最好或最佳的演算法
$\rightarrow$ 容易爛掉
### 與DP的差別
- Greedy對於每個子問題的解決方案都做出選擇,不能回退
- 動態規劃則會儲存以前的運算結果,並根據以前的結果對當前進行選擇,有回退功能
---
## Longest Increasing Subsequence(LIS)
### 想法
儲存**lis的長度為i時的最佳解**
如果可以加長就加長
否則就替換掉前面的使其為最佳解
### code
```cpp=
size_t lis(const vector<int>& s) {
if (!s.size()) return 0;
vector<int> v{s.front()};
for (const auto& i : s) {
if (i > v.back()) v.emplace_back(i);
else *lower_bound(v.begin(), v.end(), i) = i;
}
return v.size();
}
```
時間複雜度$O(nlogn)$
---
## [d221: 10954 - Add All](https://zerojudge.tw/ShowProblem?problemid=d221)
### 想法
$n$個數字總共需要$n-1$次加法操作
要使每次操作的權重最小就必須找最小的兩個數相加
### code
```cpp=
priority_queue<long long, vector<long long>, greater<long long>> pq;
for (int x, int i = 0; i != n; i++)
cin >> x, pq.emplace(x);
while (pq.size() > 1) {
auto tmp = pq.top(); pq.pop();
pq.emplace(tmp + pq.top()), pq.pop();
}
cout << pq.top();
```
時間複雜度$O(nlogn)$
---
## [c223: Add All(變異版)](https://zerojudge.tw/ShowProblem?problemid=c223)
### 想法
這題的$n$範圍更大,~~O(nlogn)的複雜度會TLE~~
> 顯然還是$O(nlogn)$,但常數變小
透過觀察可知,**每次進行操作之後的數為遞增**
如果把操作後的數字放在一個queue裡面
最小值只會發生在**原數列的沒使用過的最前面**
或是**操作後數列的最前面**
所以只需要考慮原數列的前兩個數字及操作後數列的前兩個數字
就能得知最小的兩個數字
此即為**單調對列優化**的想法
### code
```cpp=
long long n;
while (rit(n), n) {
vector<long long> v(n);
for (auto& i : v) rit(i);
sort(v.begin(), v.end());
int a = 0, end = 0, b = 0;
auto size = [&] {return end - a + n - b;};
auto val = [&] {
if (b == n) return v[a++];
if (a == end) return v[b++];
return v[a] < v[b] ? v[a++] : v[b++];
};
long long sum = 0;
while (size() > 1) {
long long x = val(), y = val();
sum += v[end++] = x + y;
}
cout << sum << endl;
}
```
---
# 進階DP
## [a344: 就只是括號而已,還敢不拿AC啊 [-續-]](http://203.64.191.163/ShowProblem?problemid=a344)
## 想法
[卡特蘭數wiki](https://zh.wikipedia.org/wiki/%E5%8D%A1%E5%A1%94%E5%85%B0%E6%95%B0)
公式為$C_n=\frac{(2n)!}{n!(n+1)!}$
所以建一個到$2n$的階乘表
然後用[數論](/@konchin/Math)教過的**模逆元**做除法
直接套公式搞定
### code
```cpp=
vector<int> fac{1, 1};
fac.resize(30001, 0);
for (int i = 2; i <= 30000; i++)
fac[i] = 1LL * fac[i - 1] * i % mod;
while (cin >> n) {
if (n & 1)
cout << "Error404" << endl;
else
cout << fac[n] * rev(1LL * fac[n / 2] * fac[n / 2 + 1]) % mod << endl;
}
```
---
## 背包問題 Knapsack problem
### 題目簡述
有一些東西,每個東西分別有其價值跟重量
有一個只能裝一定重量的包包,試問包包能裝的最大價值為何
:::info
[a279: 校長的背包問題](http://203.64.191.163/ShowProblem?problemid=a279) [a330: 主任的背包問題](http://203.64.191.163/ShowProblem?problemid=a330) [a331: 復旦的背包問題](http://203.64.191.163/ShowProblem?problemid=a331)
三題都過的電神可以自習 :poop:
:::
## 0/1 背包問題
> 在每項物品只有一個時的背包問題
### 想法
開一個$dp$陣列存當前總重量為$j$時的最大價值
對於第$i$個東西決定要不要放進包包
### 轉移式
{
$$dp[i][j]=max(dp[i-1][j],v[i]+dp[i-1][j-w[i]])$$
}
//下標字太小了所以用中括號
其中$v[i]$為第$i$個物品的價值(value)
$w[i]$為第$i$個物品的重量(weight)
### code
```cpp=
//n為物品數量 m為背包大小
vector<int> v(n), w(n);
vector<vector<int>> dp(n, vector<int>(m + 1, 0));
for (int i = 0; i < n; i++) {
for (int j = 0; j < w[i]; j++)
dp[i][j] = dp[i - 1][j];
for (int j = w[i]; j <= m; j++)
dp[i][j] = max(dp[i - 1][j], v[i] + dp[i - 1][j - w[i]]);
}
```
時間複雜度$\Theta(nm)$ 空間複雜度$\Theta(nm)$
### 滾動DP
從轉移式可以發現每次轉移都只需要用到上一$row$的資料
且轉移時必使用比$j$前面的$column$(前提是$weight$不為負
所們只要由後往前疊代dp陣列就可以壓成一維
### code
```cpp=
//n為物品數量 m為背包大小
vector<int> v(n), w(n);
vector<int> dp(m + 1, 0);
for (int i = 0; i < n; i++)
for (int j = m; j - w[i] >= 0; j--)
dp[j] = max(dp[j], v[i] + dp[j - w[i]]);
```
時間複雜度$\Theta(nm)$ 空間複雜度$\Theta(m)$
---
## 無限背包問題
> 每項物品都沒有數量限制
### 想法
從0/1背包的滾動DP解延伸,如果由前往後疊代
那已經放過的東西就可以被再放一次
### code
```cpp=
//n為物品數量 m為背包大小
vector<int> v(n), w(n);
vector<int> dp(m + 1, 0);
for (int i = 0; i < n; i++)
for (int j = w[i]; j <= m; j++)
dp[j] = max(dp[j], v[i] + dp[j - w[i]]);
```
時間複雜度$\Theta(nm)$ 空間複雜度$\Theta(m)$
---
## 有限背包問題
> 第$i$個物品有$k[i]$個
### 想法
把每項物品的數量拆成同樣的$k$個物品做0/1背包問題
### code
```cpp=
//n為物品數量 m為背包大小
vector<int> v(n), w(n), k(n);
vector<int> dp(m + 1, 0);
for (int i = 0; i < n; i++)
for (int t = 0; t != k[i]; t++)
for (int j = m; j - w[i] >= 0; j--)
dp[j] = max(dp[j], v[i] + dp[j - w[i]]);
```
時間複雜度$\Theta(knm)$ 空間複雜度$O(m)$
:::danger
好像...有點慢? :poop:
:::
### 想法
利用類似快速冪的想法,把$k$個東西拆成$2$的冪次倍
對於拆出來的東西只需要把$value$跟$weight$都乘上該倍就可以了
### code
```cpp=
//n為物品數量 m為背包大小
vector<int> v(n), w(n), k(n);
vector<int> dp(m + 1, 0);
for (int i = 0; i < n; i++) {
for (int a = 1; a < k[i]; k[i] -= a, a <<= 1) {
int weight = w[i] * a, value = v[i] * a;
for (int j = m; j - weight >= 0; j--)
dp[j] = max(dp[j], value + dp[j - weight]);
}
//剩下不是二冪次的
for (int j = m; j - w[i]*k[i] >= 0; j--)
dp[j] = max(dp[j], v[i] * k[i] + dp[j - w[i] * k[i]]);
}
```
時間複雜度$O(nmlogk)$ 空間複雜度$O(m)$
---
因為找不到旅行推銷員問題...
:poop: 自習 :poop: