區域生長演算法(手動選取種子點)MATLAB
Part0
本篇部落格主要講的是斜率優化一些最基礎的定義以及做法,高階版可以見 這篇部落格
Part1
在某些情況下,\(dp\) 的時間複雜度仍然超出了題目的限制,這時我們就要考慮對其進行優化
斜率優化是對決策進行優化的一種方法
它適用於類似 \(f[i]=min/max(a[i]×b[j]+c[i]+d[j])\) 的方程
斜率優化由一個單調佇列進行維護,維護滿足要求的最優下標,在更新當前的 \(f\) 也就是 \(dp\) 陣列時,直接取隊首即可。
接下來以幾道簡單的例題講解
例一 [APIO2010]特別行動隊
分析
題意很簡單,讓我們任意分組,使得分出來組的那一段 \(sum\) 在進行那個式子的運算之後最大,於是我們可以推轉移方程。
定義 \(f[i][j]\)
容易得到 \(f[i][j] = \max_{1\leqslant k \leqslant i}(f[i][j],f[k][j-1] + a\times (sum[i]-sum[k])^2+b\times (sum[i]-sum[k]) + c)\)
我們可以發現每次的最大值都是從之前轉移來,並且第二維是沒有意義的,所以可以轉化為一維。
要使當前被最優地更新,我們可以得到如下不等式:
\(f_{k1}+a\times (sum_i-sum_{k1})^2+b\times (sum_i-sum_{k1}) + c > f_{k2}+a\times (sum_i-sum_{k2})^2+b\times (sum_i-sum_{k2}) + c\)
然後移項使不等號一邊只剩下常數,得到: \[\frac{f_{k1}-f_{k2}+a\times sum_{k1}^2-a\times sum_{k2}^2+b\times sum_{k2}-b\times sum_{k1}}{sum_{k1}-sum_{k2}} > 2\times a\times sum_i \]
於是我們可以得到斜率為 \(2\times a\times sum_i\)。
由於 \(a<0\) ,斜率單調減,那麼維護一個上凸包即可。
程式碼
#include<bits/stdc++.h> using namespace std; #define int long long #define gc() (p1 == p2 ? (p2 = buf + fread(p1 = buf, 1, 1 << 20, stdin), p1 == p2 ? EOF : *p1++) : *p1++) #define read() ({ register int x = 0, f = 1; register char c = gc(); while(c < '0' || c > '9') { if (c == '-') f = -1; c = gc();} while(c >= '0' && c <= '9') x = x * 10 + (c & 15), c = gc(); f * x; }) char buf[1 << 20], *p1, *p2; #define db double const int maxn = 1e6+10; int x[maxn]; int f[maxn]; int sum[maxn]; int q[maxn]; int a,b,c; inline db calc(int k1,int k2){ return 1.0 * (db)(f[k1] - f[k2] - a * sum[k2] * sum[k2] + a * sum[k1] * sum[k1] + b * sum[k2] - b * sum[k1]) / (db)(sum[k1] - sum[k2]); } signed main(){ int n = read(); a = read(),b = read(),c = read(); for (int i = 1; i <= n; i++) { x[i] = read(); sum[i] = sum[i-1] + x[i]; } int head = 1,tail = 1; for(int i = 1;i <= n;++i){ while(head < tail && calc(q[head],q[head+1]) >= 2.0 * a * sum[i])head++; f[i] = f[q[head]] + a * (sum[i] - sum[q[head]]) * (sum[i] - sum[q[head]]) + b * (sum[i] - sum[q[head]]) + c; while(head < tail && calc(q[tail],q[tail-1]) <= calc(q[tail],i))tail--; q[++tail] = i; } printf("%lld\n",f[n]); return 0; }
例二 軍訓佇列
題目描述
有 \(n\) 名學生參加軍訓,軍訓的一大重要內容就是走佇列,而一個佇列的不規整程度是該隊中最高的學生的身高與最矮的學生的身高差值的平方。現在要將 \(n\) 名參加軍訓的學生重新分成 \(k\) 個佇列,每個佇列的人數不限,請求出所有佇列的不規整程度之和的最小值。
輸入格式
第一行兩個整數 \(n,k\) ,表示學生人數和佇列數。
第二行 \(n\) 個實數,表示每名學生的身高。身高範圍在 \(140∼200cm\) 之間,保留兩位小數。
輸出格式
一個實數表示答案,保留 \(2\) 位小數。
樣例
樣例輸入1:
3 2
170.00 180.00 168.00
樣例輸出1:
4.00
樣例輸入2:
5 2
170.00 180.00 168.00 140.59 199.99
樣例輸出2:
1023.36
資料範圍與提示
資料點 | \(n\) | \(k\) |
---|---|---|
\(1-2\) | \(\leqslant 10\) | \(\leqslant 5\) |
\(3-6\) | \(\leqslant 100\) | \(\leqslant 10\) |
\(7,8\) | \(\leqslant 10^5\) | \(1\) |
\(9,10\) | \(\leqslant 10^5\) | \(2\) |
\(11,12\) | \(\leqslant 10^5\) | \(3\) |
\(13,14\) | \(\leqslant 10^5\) | \(4\) |
\(15-20\) | \(\leqslant 10^5\) | \(\leqslant 20\) |
分析
首先最簡單的方法就是離散化一下,直接暴力 \(dp\) ,顯然是可以過的。
但是我們作為新時代的青年當然要搞優秀的演算法,所以考慮把暴力 \(dp\) 改成斜率優化。
與上一個題類似,狀態轉移方程仍然是二維的,轉移也很容易寫出來,即:
這裡我們需要保留第二維 \(j\) ,具體沒有什麼大影響,只需要在計算斜率的時候多傳一個參就行了。
然後我們推不等式,容易得到:
然後跟上一個題一樣單調佇列維護斜率即可。由於 \(x\) 是單調的,所以我們只需要看斜率,維護一個下凸包即可。
程式碼
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#define db double
#define rint register int
#define max(a,b) (a > b ? a : b)
#define min(a,b) (a < b ? a : b)
const int maxn = 1e5+10;
int n,k;
db a[maxn];
db f[maxn][25];
int q[maxn],head,tail;
inline db calc(int k1,int k2,int j){
return (f[k1][j-1] - f[k2][j-1] + a[k1+1] * a[k1+1] - a[k2+1] * a[k2+1]) / (a[k1+1] - a[k2+1]);
}
int main(){
scanf("%d%d",&n,&k);
if(k > n)return puts("0.00"),0;
for(rint i = 1;i <= n;++i){
scanf("%lf",&a[i]);
}
std::sort(a+1,a+n+1);
rint tot = std::unique(a+1,a+n+1) - a - 1;
head = 1,tail = 0;
for(rint i = 1;i <= tot;++i)f[i][1] = (a[1] - a[i]) * (a[1] - a[i]);
for(rint j = 2;j <= k;++j){
head = 1,tail = 0;
for(rint i = j;i <= tot;++i){
while(head<tail&&calc(q[head],q[head+1],j)<=2*a[i])head++;
f[i][j]=f[q[head]][j-1]+(a[q[head]+1]-a[i])*(a[q[head]+1]-a[i]);
while(head<tail&&calc(q[tail],q[tail-1],j)>calc(q[tail],i,j))tail--;
q[++tail]=i;
}
}
printf("%.2lf\n",f[tot][k]);
return 0;
}
例三 [APIO2014]序列分割
題目
分析
一般的套路題,分組的暴力 \(dp\) 應該非常好想,這裡不再一點一點推,然後我們會發現,每次分組是把當前組的兩部分乘起來,然後就瞬間沒了思路。
由於一個一個區間進行處理不現實,所以一定有什麼結論,然後開始推:
假設有 \(4\) 個數 \(a,b,c,d\) 組成一個序列,給出一個固定的分組 \({a},{b,c},{d}\) 。
我們會發現分組的先後順序對答案沒有影響(建議自己多舉幾個例子),然後我們就可以跟一般的分組 \(dp\) 一樣轉移了。
容易得出狀態轉移:
然後我們又開始寫斜率不等式:
\[\frac{f[k1][j-1] - f[k2][j-1] + sum[k2] \times sum[k2] - sum[k1] \times sum[k1]}{sum[k2] - sum[k1]} < sum[i] \]維護下凸包。
程式碼
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define gc() (p1 == p2 ? (p2 = buf + fread(p1 = buf, 1, 1 << 20, stdin), p1 == p2 ? EOF : *p1++) : *p1++)
#define read() ({ register int x = 0, f = 1; register char c = gc(); while(c < '0' || c > '9') { if (c == '-') f = -1; c = gc();} while(c >= '0' && c <= '9') x = x * 10 + (c & 15), c = gc(); f * x; })
char buf[1 << 20], *p1, *p2;
const int maxn = 1e5+10;
#define db double
int n,k;
int f[maxn][210],pre[maxn][210];
int sum[maxn];
int q[maxn];
inline db calc(int k1,int k2,int j){
if(sum[k1] == sum[k2])return -1e18;
return (db)(f[k1][j-1] - f[k2][j-1] + sum[k2] * sum[k2] - sum[k1] * sum[k1]) / (db)(sum[k2] - sum[k1]);
}
inline void print(int x,int gs){
if(gs == 0)return;
print(pre[x][gs],gs-1);
printf("%lld ",x);
}
signed main(){
n = read(),k = read();
for (int i = 1; i <= n; i++)sum[i] = sum[i-1] + read();
int head = 1,tail = 1;
for(int j = 1;j <= k;++j){
head = tail = 0;
for(int i = j;i <= n;++i){
while(head < tail && calc(q[head],q[head+1],j) <= sum[i])head++;
pre[i][j] = q[head];
f[i][j] = f[q[head]][j-1] + sum[q[head]] * (sum[i] - sum[q[head]]);
while(head < tail && calc(q[tail],q[tail-1],j) >= calc(q[tail],i,j))tail--;
q[++tail] = i;
}
}
printf("%lld\n",f[n][k]);
int nn = n;
for(int i = k;i >= 1;--i){
nn = pre[nn][i];
printf("%lld ",nn);
}
return 0;
}
Part2
高階一些的斜率優化 \(dp\) ,先咕咕咕(逃