作為數學渣,先複習一下已知兩點\((x_1, y_1)\), \((x_2, y_2)\),怎麼求過兩點的一次函式的斜率...
待定係數法代入 \(y = kx + b\) 有:
\(x_1k + b = y_1\)
\(x_2k + b = y_2\)
兩式相減有:
\(k = \frac\)
故事圍繞著《演算法競賽高階指南》的三一道例題展開:
任務安排 1:
假如我們啟動了乙個任務\([l, r]\),那麼它會對後面造成\(s * \sum_^ c_i\)的費用。
設\(st\)為 \(t\) 的字首和,設\(sum\)為\(c\)的字首和
\(f[i]\) 表示安排完前 \(i\) 個任務的最小花費:
$f[i] = min(f[j] + (sum[i] - sum[j]) * t[i] + (sum[n] - sum[j]) * s) $
時間複雜度\(o(n ^ 2)\)
任務安排 2:
將上題推出的轉移式子得\(min\)去掉觀察:
\(f[i] = f[j] + (sum[i] - sum[j]) * t[i] + (sum[n] - sum[j]) * s\)
發現我們無法優化\(dp\)的原因是有與 \(i, j\) 兩者都有關的乘積項,導致我們沒有最優策略:
\(- sum[j] * t[i]\)
考慮把這個式子拆開轉換為一次函式:\(y = kx + b\) 的形式。
則以上式子可以化成:
\(\underline_y = \underline_k * \underline_x + \underline_b\)
發現當 \(i\) 確定後,該一次函式的斜率 \(k\) 確定,則截距 \(b\) 越小, \(f[i]\) 越小。
我們將 \((x, y)\) 即 \((sum[j], f[j])\) 放在座標系上。
則形象化可理解為一條直線從下往上平移,所碰到的第乙個點即為最優解。
發現乙個點如果被另外兩個點圍起來,永遠不可能作為最優解。
刪除了這些點後,發現相鄰點之間的斜率為單調遞增的,即構成乙個凸包:
發現乙個斜率 \(k\) 固定的直線所匹配的最優點滿足:
由於這道題斜率 \(t[i] + s\)、橫座標 \(sum[j]\) 皆單調遞增。
由於橫座標遞增,所以維護凸包時,每當加入乙個點時:
\(\frac - y_} - x_} >= \frac - y_} - x_}\)
由於斜率遞增,所以 \(i + 1\) 的最優解一定在 \(i\) 的右邊,所以一旦隊頭兩個點構成的斜率 $ < $ 當前的斜率,可以彈出隊頭。即滿足:
\(\frac - y_} - x_} < t[i] + s\)
然後隊頭的元素即為最優選擇。
時間複雜度 \(o(n)\)
\(tips:\)
由於除法會有精度問題,可以通過交叉相乘的形式比較大小
#include #include #include #define x(a) (c[a])
#define y(a) (f[a])
#define k(a) (t[a] + s)
using namespace std;
typedef long long ll;
const int n = 300005;
int n, s;
ll t[n], c[n], q[n], f[n];
int main()
printf("%lld\n", f[n]);
return 0;
}
任務安排3
此時的斜率不再遞增了,也就是我們不能\(pop\_front\)了,不過我們仍可以維護凸包,然後保持單調性,二分。
時間複雜度\(o(nlog_2n)\)
#include #include #include #define x(a) (c[a])
#define y(a) (f[a])
#define k(a) (t[a] + s)
using namespace std;
typedef long long ll;
const int n = 300005;
int n, s, t[n], c[n], q[n];
ll f[n];
int main()
f[i] = f[q[r]] + (ll)(c[i] - c[q[r]]) * t[i] + (ll)(c[n] - c[q[r]]) * s;
while(hh < tt && ((y(q[tt]) - y(q[tt - 1])) * (x(i) - x(q[tt])) >= ((y(i) - y(q[tt])) * (x(q[tt]) - x(q[tt - 1]))))) tt--;
q[++tt] = i;
}printf("%lld\n", f[n]);
return 0;
}
運輸小貓
設 \(d[i]\) 為從 \(1\) 走到 \(i\) 的距離。
那麼每條小貓最佳的出發時間應為 \(a[i] = t[i] - d[h[i]]\),如果要接上這只貓,必須大於這個時間出發。
我們將 \(a\) 陣列排序,那麼問題等價轉換於把 \(m\) 個點劃分成 \(p\) 個連續區間,使每一段到右端點的距離之和的總和最小。(內心 \(os\):這不就是擺渡車的變種嗎?)
那麼樸素 \(dp\) 便很好列出了:
\(f[k][i]\) 表示將前 \(i\) 只小貓分成 \(k\) 組的最小總和。
設 \(suma\) 為 \(a\) 陣列的字首和。
\(f[k][i] = min(f[k - 1][j] + a[i] * (i - j) - suma[i] + suma[j]) (0 <= j < i)\)
由於這裡面有乙個非常討厭的 \(a[i] * -j\),所以我們考慮斜率優化:
\(\underline_y = \underline_k * \underline_x + \underline_b\)
發現這裡的橫座標、斜率都是單調遞增,即情況 \(1\)。那麼我們可以將不需要的直接踢出即可。
時間複雜度 \(o(pm)\)
#include #include #include #include using namespace std;
typedef long long ll;
const int n = 100005, s = 105;
int n, m, p, d[n], a[n], q[n];
ll f[s][n], sum[n];
ll inline y(int i, int k)
int main()
sort(a + 1, a + 1 + m);
for (int i = 1; i <= m; i++) sum[i] = sum[i - 1] + a[i];
for (int k = 1; k <= p; k++)
}printf("%lld\n", f[p][m]);
}
斜率優化DP學習筆記
本文以luogup3195 玩具裝箱為例,我們很容易可以的出下面這個柿子 f i min 設 b i s i i j 為 f i 的最優決策點,則有 f i f j b i b j l 1 2 把只與 j 有關的放在左邊 f j b j 2 b j l 1 2 b i b j b i 2 l 1 2...
斜率優化dp筆記
瞎扯 演算法真的是無止境,從暴力到dp原本以為很神奇了,沒想到還能優化dp,而且是把o n 2 變成o n 真是無 說。引入 我們來分析這麼乙個問題,給你n個數,要你把他們分成連續的若干塊,使得讓他們的每段和的平方加起來最小.正常我們會想到的就是o n 2 的dp,方程就是 dp i min dp ...
斜率優化DP 學習筆記 更新中
參考資料 1.元旦集訓的課件已經很好了 2.一 對於一類轉移方程 f i max a i 和c i 是開始求解前就知道常數,b j 和d j 知道f j 後就知道有關 可以使用斜率優化 不是這個形式就盡量往這個形式化 決策單調性 對於兩個轉移j和k,設b j 假設j比k優或相等,把式子一化就變成了 ...