3351 字
9 分钟
为什么说 dp 是恐怖的?
2025-01-20

小编带你来了解下 dp:

https://www.luogu.com.cn/article/343e16kz

https://oi-wiki.org/dp/opt/slope-trick/

https://www.luogu.com.cn/training/559292#information

https://www.luogu.com.cn/article/bn3a465i

https://www.luogu.com.cn/article/ki71nw88

https://qalxry.github.io/2024/01/20/%E3%80%90%E7%AE%97%E6%B3%95%E3%80%91DP%E4%BC%98%E5%8C%96%E6%80%BB%E7%BB%93/

结构#

线性#

区间#

背包#

树 & 图#

树形 dp#

P2279 消防局的设立#

方法一#

fi,0/1/2/3/4f_{i,0/1/2/3/4}为以ii为子树,保证能点亮 下两层/下一层/自己/父亲/上二 及以下的所有节点 的最小代价。

转移比较简单。

方法二#

fi,0/1/2/3/4f_{i,0/1/2/3/4}为以ii为子树,存在ii的 下下层儿子/儿子/自己/父亲/往上距离为 2 的点将其点亮的最小代价。

转移读者自行尝试,如果不会,可以去问连神,chm_qwq

虽然不知道和方法一有什么区别。

方法三#

贪心,我们考虑在刚好能点亮的位置点灯。下面设DD为一个灯最远能点亮的距离。

于是设gig_{i}为以ii为子树最远没有被点亮的距离;fif_i为以ii为子树最近被点亮的点的距离。

根据定义有:gi=maxisonugi+1g_i=\max_{i\in son_u} g_i+1fi=minisonufi+1f_i=\min_{i\in son_u} f_i+1

于是分三种情况。

  • gi+fiDg_i+f_i \le D:可以直接点亮,于是gi=infg_i=-inf
  • 如果这个点为必须点亮而没有被点亮,既fi>Df_i>D,那么gi=max{0,gi}g_i=\max\{0,g_i\}既要求其他点给这个点点亮。
  • gi=Dg_i=D到最远距离了,直接在该点放灯塔,那么gi=inf,fi=0g_i=-inf,f_i=0
ex方法一/二#

若在点ii放灯要代价aia_i

fi,jf_{i,j}为以ii为子树,向上至少能点亮jj层的最小代价。

gi,jg_{i,j}为以jj为子树,向下至少能点亮jj层的最小代价。

转移读者自推不难。

状压#

技巧#

枚举子集O(3n)O(3^n)#

for(int T = (S - 1) & S;T;T = (T - 1) & S){
//真子集要从S开始。
}

高维前缀和O(n2n)O(n 2^n)#

子集#

g(S)=TSf(T)g(S)=\sum_{T\subseteq S} f(T)

eg:fsf_s表示有几个满足 aisa_i \subseteq s

维护有三种理解。

去重法#

最简单的想法是去掉一个转移:

for(int j = 0;j < (1 << n);j++)
for(int i = 0;i < n;i++)
if(j >> i & 1) f[i] += f[i ^ (1 << j)];

然而会重,换枚举顺序即可。

for(int i = 0;i < n;i++)
for(int j = 0;j < (1 << n);j++)
if(j >> i & 1) f[i] += f[i ^ (1 << j)];
dp法#

为了去重,我们设fi,sf_{i,s}表示只允许右ii位可与ss不同(但是仍为ss的子集)的和。

  • fi,s=fi1,s(i∉s)f_{i,s}=f_{i-1,s}(i \not \in s)
  • fi,s=fi1,s+fi1,s2i(is)f_{i,s}=f_{i-1,s} + f_{i-1,s \oplus 2^i}(i\in s)

ii滚掉即可得到代码。

类比法#

看三维前缀和:

for(int i = 1;i <= n;i++) for(int j = 1;j <= n;j++) for(int z = 1;z <= n;z++)
f[i][j][z] += f[i - 1][j][z];
for(int i = 1;i <= n;i++) for(int j = 1;j <= n;j++) for(int z = 1;z <= n;z++)
f[i][j][z] += f[i][j - 1][z];
for(int i = 1;i <= n;i++) for(int j = 1;j <= n;j++) for(int z = 1;z <= n;z++)
f[i][j][z] += f[i][j][z - 1];

若扩展到nn维也是如此。

假如每维只有22呢,那么ff就可以状压成fsf_s,然后枚举每一位即可。

超集#

g(S)=STf(T)g(S)=\sum_{S\subseteq T} f(T)

fsf_s表示有几个满足 sais \subseteq a_i

for(int i = 0;i < n;i++)
for(int j = 0;j < (1 << n);j++)
if(!(j >> i & 1)) f[i] += f[i ^ (1 << j)];
差分#

差分回去的话,直接把加号改成减号。

P3959 宝藏#

好题。核心技巧是枚举子集,妙处在状态设计。

因为加路径需要到起点的距离,那么我们可以看成一滴水从某个顶点开始散开,一次性添加需要的同一层的所有点。

于是我们先处理出fi,jf_{i,j}表示当前点集为ii,扩展的点集为jj的最小路径长度和。

const int S = (1 << n) - 1;
for(int i = 2;i <= S;i++) lg[i] = lg[i >> 1] + 1;
for(int i = 1;i <= S;i++){
cnt = 0;
for(int j = (S ^ i) , _ = (S ^ i);j;j = (j - 1) & _)
tmp[++cnt] = j;
for(int j = cnt;j >= 1;j--){
int lowbit = (tmp[j] & -tmp[j]) , v = inf;
for(int z = 0;z < n;z++) if(i >> z & 1) v = min(v , mp[lg[lowbit]][z]);
f[i][tmp[j]] = add(f[i][tmp[j] ^ lowbit] , v);
}
}

然后求出gi,jg_{i,j}表示共有ii层,点集为jj,显然gi,j=sjgi1,js+fjs,jg_{i,j}=\sum_{s\in j} g_{i-1,j\oplus s}+f_{j\oplus s,j}

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int inf = 1e9;
int n , m;
int mp[20][20];
int f[4100][4100] , g[14][4100];
int lg[4100];
int tmp[4100] , cnt;
inline int add(int x , int y){ return (x > inf - y? inf : x + y); }
inline int mul(int x , int y){ return (x > inf / y? inf : x * y); }
int main(void){
scanf("%d%d" , &n , &m);
memset(mp , 0x3f , sizeof(mp));
memset(g , 0x3f , sizeof(g));
while(m--){
int u , v , w; scanf("%d%d%d" , &u , &v , &w);
u--; v--;
mp[u][v] = min(mp[u][v] , w);
mp[v][u] = min(mp[v][u] , w);
}
const int S = (1 << n) - 1;
for(int i = 2;i <= S;i++) lg[i] = lg[i >> 1] + 1;
for(int i = 1;i <= S;i++){
cnt = 0;
for(int j = (S ^ i) , _ = (S ^ i);j;j = (j - 1) & _)
tmp[++cnt] = j;
for(int j = cnt;j >= 1;j--){
int lowbit = (tmp[j] & -tmp[j]) , v = inf;
for(int z = 0;z < n;z++) if(i >> z & 1) v = min(v , mp[lg[lowbit]][z]);
f[i][tmp[j]] = add(f[i][tmp[j] ^ lowbit] , v);
}
}
// printf("%d\n" , f[1][8]);
for(int i = 0;i < n;i++) g[0][1 << i] = 0;
for(int k = 1;k < n;k++) for(int i = 1;i <= S;i++)
for(int j = i;j;j = (j - 1) & i)
g[k][i] = min(g[k][i] , add(g[k - 1][i ^ j] , mul(f[i ^ j][j] , k)));
int ans = inf;
for(int k = 0;k < n;k++) ans = min(ans , g[k][S]);
printf("%d\n" , ans);
}

P1357 花园#

首先可以得:fi,jf_{i,j}为前ii个花后mm个为jj的方案,转移显然。环可以看成有n+mn+m朵花,前后相同。

然后发现ff均有前一状态转移而来,并且可转移的点相同,那么可以写成矩阵,矩阵快速幂优化即可。

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
ll n; int m , k , ans;
const int N = 32;
struct QWQ {
int a[34][34];
inline QWQ(){ memset(a , 0 , sizeof(a)); }
inline friend QWQ operator * (const QWQ x , const QWQ y){
QWQ ret;
for(int i = 0;i < N;i++) for(int j = 0;j < N;j++)
for(int k = 0;k < N;k++)
if((ret.a[i][j] += (ll)x.a[i][k] * y.a[k][j] % MOD) >= MOD)
ret.a[i][j] -= MOD;
return ret;
}
}base , qwq;
inline QWQ qpow(QWQ x , ll y){
QWQ ret;
for(int i = 0;i < N;i++) ret.a[i][i] = 1;
while(y){
if(y & 1) ret = ret * x;
x = x * x; y >>= 1;
}
return ret;
}
int main(void){
scanf("%lld%d%d" , &n , &m , &k);
const int S = (1 << m) - 1;
for(int j = 0;j <= S;j++){
if(__builtin_popcount(j) > k) continue;
base.a[(j << 1) & S][j] = 1;
if(__builtin_popcount((j << 1 | 1) & S) <= k)
base.a[(j << 1 | 1) & S][j] = 1;
}
qwq = qpow(base , n);
for(int s = 0;s <= S;s++) if(__builtin_popcount(s) <= k)
if((ans += qwq.a[s][s]) >= MOD) ans -= MOD;
printf("%d\n" , ans);
}

P5369 [PKUSC2018] 最大前缀和#

难点在转换。

考虑枚举可能的最大前缀和,显然有2n2^n种,对于每种合法的最大前缀和(前ii个为最大):

  • 1<ji1<j \le i满足k=jiak0\sum_{k=j}^i a_k \ge 0
  • i<jni < j \le n满足k=jiak<0\sum_{k=j}^i a_k < 0

原因显然。发现前后并不冲突,于是可以分开dp。

对于第一种,可以前插,条件是插入前的和大于等于 0。

对于第二种,可以后插,条件是插入后的和小于 0。

#include <cstdio>
#include <algorithm>
using namespace std;
const int MOD = 998244353;
int n , f[1100005] , g[1100005];
long long sum[1100005];
//f:后缀和的真子集都大于等于0的方案数
//g:前缀和都小于0的方案数
int main(void){
scanf("%d" , &n);
for(int i = 0;i < n;i++) scanf("%lld" , &sum[1 << i]);
const int S = (1 << n) - 1;
for(int s = 1;s <= S;s++) sum[s] = sum[s ^ (s & -s)] + sum[s & -s];
g[0] = 1;
for(int i = 0;i < n;i++) f[1 << i] = 1;
for(int s = 0;s <= S;s++) for(int i = 0;i < n;i++){
if(s >> i & 1) continue;
if(sum[s] >= 0) (f[s | (1 << i)] += f[s]) %= MOD;
if(sum[s | (1 << i)] < 0) (g[s | (1 << i)] += g[s]) %= MOD;
}
int ans = 0;
for(int s = 0;s <= S;s++) (ans += ((sum[s] % MOD + MOD) % MOD) * f[s] % MOD * g[S ^ s] % MOD) %= MOD;
printf("%d\n" , ans);
}

CF449D & P6442#

P6442:要求或起来为全集的方案数,不妨取反,于是变成求与起来为空集的方案数。

先求出fsf_s表示有多少个数有ss

然后gsg_s表示与起来为ss超集的方案数,显然为2fs12^{f_s}-1

然后差分回去就可以求出与起来为00的方案数了。

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
int n , a[2000005];
ll f[2000005];
inline ll qpow(ll x , ll y){ ll ret = 1; while(y){ if(y & 1) ret = ret * x % MOD; x = x * x % MOD; y >>= 1; } return ret; }
int main(void){
scanf("%d" , &n);
for(int i = 1;i <= n;i++) scanf("%d" , &a[i]) , f[a[i]]++;
const int S = (1 << 20) - 1;
for(int i = 0;i < 20;i++) for(int j = 0;j <= S;j++)
if(!(j >> i & 1)) f[j] += f[j ^ (1 << i)];
for(int s = 0;s <= S;s++) f[s] = (qpow(2 , f[s]) - 1 + MOD) % MOD;
for(int i = 0;i < 20;i++) for(int j = 0;j <= S;j++)
if(!(j >> i & 1)) (f[j] += MOD - f[j ^ (1 << i)]) %= MOD;
printf("%lld\n" , f[0]);
}

CF1208F#

比较喵的一道题。

可以枚举第一个数,考虑最大化,于是贪心从高位取。

每次判断是否存在两个不同且下标大于 i 的数的与是当前枚举的超集。

这个可以用高维前缀和预处理。

fsf_s表示为ss超集的下标最大值与次大值。

#include <cstdio>
#include <algorithm>
using namespace std;
int n , a[1000005];
struct QWQ {
int a , b; //a > b
inline QWQ(int _ = 0 , int __ = 0){ a = _; b = __; }
inline QWQ friend operator + (const QWQ p , const QWQ q){
if(p.a < q.a) return QWQ(q.a , max(p.a , q.b));
return QWQ(p.a , max(p.b , q.a));
}
}f[2097154];
int main(void){
scanf("%d" , &n);
for(int i = 1;i <= n;i++){
scanf("%d" , &a[i]);
f[a[i]] = f[a[i]] + QWQ(i , 0);
}
const int S = (1 << 21) - 1;
for(int i = 0;i < 21;i++) for(int s = 0;s <= S;s++)
if(!(s >> i & 1)) f[s] = f[s] + f[s ^ (1 << i)];
int ans = 0;
for(int i = 1;i <= n - 2;i++){
int nw = 0;
for(int j = 20;j >= 0;j--){
if(a[i] >> j & 1) continue;
if(f[nw | (1 << j)].b > i) nw |= (1 << j);
}
ans = max(ans , (nw | a[i]));
}
printf("%d\n" , ans);
}

计数 & 概率#

形式#

括号 :(#

  • 看成(..)(..)(..)...(...)dp
  • 拆第一个成对括号,然后剩下的是同样的形式。
  • (看成 1,)看成 -1。前缀和非负。

基环树#

前置:树形dp

这是非树,但是有很好的性质的特殊图,他删掉一条边可以变为树,而且有且仅有一个环。

只要满足一个联通块内恰好有 n 条边,那么这个联通块就是基环树。

一般有两种 dp 方法:

是环#

由于只有一个环,那么直接将环缩成点,就是树了,最后再在环上特殊处理即可。

是边#

删掉一条边即可变为树,于是先删掉一条反祖边,树形 dp,特殊处理。

技巧#

  • 用并查集找反祖边。
  • dfs找环。

小试#

a. P4381 Island#

可以转换成基环树求直径问题。

定义:基环树的直径为基环树上最长路径,非两点间最短路径。

首先找出环,标记出来,这样就有了若干棵子树,对每棵子树求直径和从根节点出发的最长路径。

如果把最长路径当做环的点权,那么问题就变成在环上找一条路径,路径权值为经过点权和与路径和,不能重复走点,求最长路径。

显然,断环成链,把节点复制一份,把边权算到右边的点权上,这样就变成最大区间和了,单调队列即可做。

(偷了张博客的图。。)

#include <cstdio>
#include <algorithm>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
struct ZBJ { int x , w , id; };
vector <ZBJ> g[1000005];
int n;
int _f[1000005]; pair <int , int> ni[1000005]; int ni_cnt;
int find(int x){ return _f[x] == x? x : _f[x] = find(_f[x]); }
int huan[1000005] , onhuan[1000005] , tot;
ll a[2000005] , b[2000005];
bool find_huan(int x , int lt){
if(huan[x] == 2){ return 1; }
huan[x] = 2;
for(ZBJ nxt : g[x]) if(nxt.id != lt){
if(find_huan(nxt.x , nxt.id)){
huan[x] = 1; onhuan[++tot] = x;
b[tot] = nxt.w;
return 1;
}
}
huan[x] = -1;
return 0;
}
ll ansd;
pair <ll , ll> dp[1000005]; //直径
void dfs(int x , int lt){
dp[x] = {0 , 0};
for(ZBJ nxt : g[x]) if(nxt.id != lt && huan[nxt.x] != 1){
dfs(nxt.x , nxt.id);
const ll t1 = dp[nxt.x].first + nxt.w;
if(t1 >= dp[x].first)
dp[x].second = dp[x].first , dp[x].first = t1;
else if(t1 > dp[x].second)
dp[x].second = t1;
}
ansd = max(ansd , dp[x].first + dp[x].second);
}
pair <ll , int> q[1000005]; int tl , hd;
inline ll solve(int x){
ansd = 0; tot = 0; hd = 1; tl = 0;
find_huan(ni[x].first , 0);
for(int i = 1;i <= tot;i++){
dfs(onhuan[i] , 0);
a[i + tot] = a[i] = dp[onhuan[i]].first;
// if(n == 15) printf("%d %d\n" , onhuan[i] , a[i]);
}
b[tot + 1] = b[1];
b[1] = 0;
for(int i = 2;i < tot;i++) b[i + tot] = b[i];
int m = tot + tot - 1;
// for(int i = 1;i <= m;i++) printf("%d:%d %d\n" , i , a[i] , b[i]);
// puts("");
for(int i = 1;i <= m;i++) b[i] += b[i - 1];
ll ret = ansd;
for(int i = 2;i <= tot;i++){
while(tl >= hd && q[tl].first <= b[i] + a[i]) tl--;
q[++tl] = {b[i] + a[i] , i};
}
for(int i = 1;i <= tot;i++){
while(tl >= hd && q[hd].second <= i) hd++;
// printf("%d\n" , q[hd].first);
ret = max(ret , a[i] - b[i] + q[hd].first);
while(tl >= hd && q[tl].first <= b[i + tot] + a[i + tot]) tl--;
q[++tl] = {b[i + tot] + a[i + tot] , i + tot};
}
for(int i = 0;i <= m + 1;i++) a[i] = b[i] = 0;
// printf("ret%d\n" , ret);
return ret;
}
int main(void){
scanf("%d" , &n);
for(int i = 1;i <= n;i++) _f[i] = i;
for(int i = 1 , v , w;i <= n;i++){
scanf("%d%d" , &v , &w);
g[i].push_back((ZBJ){v , w , i});
g[v].push_back((ZBJ){i , w , i});
if(find(i) == find(v)){ ni[++ni_cnt] = {i , v}; continue; }
_f[find(i)] = find(v);
}
ll ans = 0;
for(int i = 1;i <= ni_cnt;i++) ans += solve(i);
printf("%lld\n" , ans);
}

b. P2607 骑士#

比较简单,因为不保证联通,于是是基环森林,每个联通快都是 n 条边,于是是基环树。

又因为相互没有影响,于是分开考虑。发现当成树的子问题是最大独立集,简单树形 dp 可做,再考虑反祖边,这两点不能同时选,那么强制某个点不选再取最大即可。

排列类#

https://www.luogu.com.cn/article/xt2szowt

直接按照下标转移的暂时不说。

插入法#

我们按照值从小到大插入,即fif_i表示插入了小于等于ii的所有数的答案。

AT_dp_t Permutation#

fi,jf_{i,j}表示前ii个,前一个插的排名为jj,转移不难。

连续段dp#

https://www.cnblogs.com/chroneZ/p/17938137

https://www.cnblogs.com/stinger/p/16471744.html

https://www.cnblogs.com/best-brain/p/18006547

有点像扫描线,我们以加入顺序为横轴,位置为纵轴画如上图,然后按照大小顺序从下往上扫(上往下,下往上都可)。

我们把连在一起的称为一个连续段,连续段 dp 是通过连续段的数量来进行 dp。

有以下三种情况。

延伸(2->2) 合并 新建(2->3)

好像不对。。。

P5999 [CEOI 2016] kangaroo#

求波浪型排列(其差分正负交错,没有连续的正或负),满足p1=s,pn=tp_1=s,p_n=t的方案数。

显然可以从小往大插,设fi,jf_{i,j}表示插入了前ii个数,形成了jj个段的方案数。

我们枚举i(is,it)i(i\not = s,i \not = t)

  • 新建段:

对于fi1,j1f_{i-1,j-1},它有jj个空位,当我们放入ii时,显然比之前放的都大,这里没啥限制,都是合法的。

唯一的限制是p1=s,pn=tp_1=s,p_n=t,所以当i+1>si+1>s时,p1p_1被放了,空位减,尾同理。

所以(j[i>s][i>t])fi1,j1fi,j(j-[i>s]-[i>t])f_{i-1,j-1} \to f_{i,j}

  • 段添加:

我们不能允许这种操作,因为如果允许了,因为只能往块的头尾加,这样一定不满足波浪型限制,不必转移。

  • 段合并:

无论放哪,两侧相邻的都比ii小,合成的新的块仍满足此性质。

jfi1,j+1fi,jjf_{i-1,j+1} \to f_{i,j}

答案即为fn,1f_{n,1}

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int MOD = 1e9 + 7;
int n , s , t;
int f[2005][2005];
int main(void){
scanf("%d%d%d" , &n , &s , &t);
f[0][0] = 1;
for(int i = 1;i <= n;i++) for(int j = 1;j <= i;j++){
if(i == s || i == t){
f[i][j] = (f[i - 1][j] + f[i - 1][j - 1]) % MOD;
continue;
}
f[i][j] = (1LL * j * f[i - 1][j + 1] % MOD + 1LL * (j - (i > s) - (i > t)) * f[i - 1][j - 1] % MOD) % MOD;
}
printf("%d\n" , f[n][1]);
}

CF1515E#

fi,jf_{i,j}加入了前ii个点,一共有jj个连续段。

  • 新建:fi,j×(j+1)fi+1,j+1f_{i,j}\times(j+1)\longrightarrow f_{i+1,j+1}
    • 系数:有(j+1)(j+1)个空位。
    • 为什么能加:不知道

优化#

单调队列#

决策单调性#

https://www.cnblogs.com/alex-wei/p/DP_optimization_method_II.html

https://www.cnblogs.com/birchtree/p/12937975.html

四边形不等式#

定义1:若对于abcd\forall a\le b \le c \le d,满足w(a,d)+w(b,c)w(a,c)+w(b,d)w(a,d)+w(b,c)\ge w(a,c)+w(b,d)。则称ww满足四边形不等式。

下面的推论比较好用。

推论1.1:对于a<b\forall a < b,满足w(a,b+1)+w(a+1,b)w(a,b)+w(a+1,b+1)w(a,b+1)+w(a+1,b)\ge w(a,b)+w(a+1,b+1)ww满足四边形不等式。

证明:设a<c,a+1<ca<c,a+1<c可得两式,相加可证a<b<c,w(a,c+1)+w(b,c)w(a,c)+w(b,c+1)a<b<c,w(a,c+1)+w(b,c)\ge w(a,c)+w(b , c+1),同样可证出

w(a,d)+w(b,c)w(a,c)+w(b,d)w(a,d)+w(b,c)\ge w(a,c)+w(b,d),所以满足四边形不等式。

推论1.2:若ww满足四边形不等式,那么ww满足凸完全单调性,即ww是凸函数。

从四边形不等式到决策单调性#

定义2:对于fi=min{fj+w(j,i)0j<i}f_i=\min\{f_j+w(j,i)| 0 \le j < i\}的状态转移方程,若j=pij=p_i时可让fif_i取得最小值,则pip_ifif_i的最优决策点。若pip_i单调不降,那么称ff具有决策单调性。

定理2.1:若ww满足四边形不等式,那么ff具有决策单调性。

证明:

i[1,n],j[0,pi)\forall i \in [1,n],j\in[0,p_i),根据pip_i的定义得:f_{p_i}+w(p_i,i)\le f_j+w(j,i)\tag 1

i(i,n]\forall i'\in(i,n],此时j<pi<i<ij<p_i<i<i',所以w(j,i)+w(pi,i)w(j,i)+w(pi,i)w(j,i')+w(p_i,i)\ge w(j,i)+w(p_i,i')

移项得:w(p_i,i')-w(p_i,i)\le w(j,i')-w(j,i) \tag 2

(1)+(2)(1)+(2)得:fpi+w(pi,i)fj+w(j,i)f_{p_i}+w(p_i,i')\le f_j+w(j,i')

所以j<pij<p_i的一定没有决策点pip_i优,于是fif_{i'}的最优决策点pipip_{i'}\ge p_i,所以ff具有决策单调性。

大胆假设#

一般题目可以通过枚举(瞎猜)来验证四边形不等式,无需严格证明。

然后你就拥有了一个很强的性质:决策单调性。于是你可以使用以下几种方式优化成O(nlogn)O(n\log n)

以分治#

适用范围:ff转移不与当前层的ff有关。

比较简单的想法,由于决策单调性,所以对于fif_i可能的决策点是一个区间。

可以利用分治先求出fmidf_{mid}再求出左右区间的决策点范围。

void solve(int l , int r , int ql , int qr){
int mid = (l + r) >> 1 , k = ql;
for(int i = ql , _ = min(mid - 1 , qr);i <= _;i++) if(v(i , mid) > p[mid])
p[mid] = v(i , mid) , k = i;
if(l < mid) solve(p , l , mid - 1 , ql , k);
if(mid < r) solve(p , mid + 1 , r , k , qr);
}

以二分队列#

适用范围:决策点单调不降。

考虑每个点可能作为最优决策点的范围,容易证明一定成区间。

我们可以用(x,l,r)(x,l,r)表示决策点xx可能作为[l,r][l,r]ff的最优决策点,放进队列里。

然后分三步:

  1. 无法成为fif_i的最优决策点的出队列。
  2. 更新fif_i
  3. 出队。
    1. 加入队尾严格劣于ii,那么这个决策点一定不优。这里可以通过对ll的贡献大小判断。
    2. 空队列,直接加(i,i+1,n)(i,i+1,n)
    3. 如果队尾部分优于ii,那么需要修改队尾的区间。由于区间连续,则一定存在一点满足左边队尾优,右边ii优,那么可以二分找出来。
struct SB{ int x, l , r; };
deque <SB> q;
q.push_back((SB){0 , 1 , n});
for(int i = 1;i <= n;i++){
while(!q.empty() && q.front().r < i) q.pop_front();
q.front().l = i;
f[i] = v(p[i] = q.front().x , i);
while(!q.empty() && v(i , q.back().l) <= v(q.back().x , q.back().l)) q.pop_back();
if(q.empty()){ q.push_back((SB){i , i + 1 , n}); continue; }
int l = q.back().l , r = q.back().r + 1 , ret = r;
while(l <= r){
int mid = (l + r) >> 1;
if(v(i , mid) <= v(q.back().x , mid)){ ret = mid; r = mid - 1; }
else l = mid + 1;
}
if(ret <= n){
q.back().r = ret - 1;
q.push_back((SB){i , ret , n});
}
}

以二分栈#

斜率优化#

一些只和i,ji,j有关的项和一个和i,ji,j都有关的项的和,一般可以使用斜率优化。

例如:

fi=max{fj+ai+aj+bibj}f_i=\max\{f_j+a_i+a_j+b_ib_j\}

假如ii一定,可以看成关于jj的函数。

fiaibibj=fj+ajf_i-a_i-b_ib_j=f_j+a_j

观察b+kx=yb+kx=y,得:

b=fiai,k=bi,x=bj,y=fj+ajb=f_i-a_i,k=-b_i,x=b_j,y=f_j+a_j

现在已知所有的(x,y)(x,y),要求出最大的fi=b+aif_i=b+a_i,因为ai,ka_i,k是常数,所以相当于找最大的bb使得直线能经过一个点(x,y)(x,y)

尝天下鲜#

P1912 小G诗人#

si=i+sjs_i=i+\sum |s_j|

所以fi=min{fj+sisj1LP}f_i=\min\{f_j+|s_i-s_j-1-L|^P\}

猜测满足四边形不等式,于是具有决策单调性。

所以直接用二分队列优化就行了。

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef long double ll;
const ll MAX = 1000000000000000007;
int T , n , L , P;
char str[3200005]; int _;
struct ZBj{
int st , le;
inline void read(){
st = _;
scanf("%s" , str + st);
le = strlen(str + st);
// printf("%d %d %s\n" , _ , le , str + st);
_ += le + 2;
}
}a[100005]; int s[100005];
ll f[100005]; int p[100005];
inline ll mul(ll x , ll y){ return x * y; }
inline ll add(ll x , ll y){ return x + y; }
inline ll qpow(ll x , int y){
if(x == 0) return 0;
ll ret = 1;
while(y){
if(y & 1) ret = mul(ret , x);
x = mul(x , x); y >>= 1;
}
return ret;
}
inline ll v(int j , int i){ return add(f[j] , qpow(abs(s[i] - s[j] - 1 - L) , P)); }
void print(int x){
if(x <= 0) return ;
print(p[x]);
for(int i = p[x] + 1;i <= x;i++) printf("%s%c" , str + a[i].st , i == x? '\n' : ' ');
}
struct SB{ int x, l , r; };
deque <SB> q;
inline void solve(){
_ = 0;
scanf("%d%d%d" , &n , &L , &P);
for(int i = 1;i <= n;i++) a[i].read();
for(int i = 1;i <= n;i++) s[i] = s[i - 1] + a[i].le + 1;
f[0] = 0;
q.clear();
q.push_back((SB){0 , 1 , n});
for(int i = 1;i <= n;i++){
while(!q.empty() && q.front().r < i) q.pop_front();
q.front().l = i;
f[i] = v(p[i] = q.front().x , i);
while(!q.empty() && v(i , q.back().l) <= v(q.back().x , q.back().l)) q.pop_back();
if(q.empty()){ q.push_back((SB){i , i + 1 , n}); continue; }
int l = q.back().l , r = q.back().r + 1 , ret = r;
while(l <= r){
int mid = (l + r) >> 1;
// printf("%d %d %d\n" , mid , q.back().x , i);
if(v(i , mid) <= v(q.back().x , mid)){ ret = mid; r = mid - 1; }
else l = mid + 1;
}
if(ret <= n){
q.back().r = ret - 1;
q.push_back((SB){i , ret , n});
}
}
if(f[n] > 1000000000000000000) puts("Too hard to arrange");
else{
printf("%.0LF\n" , f[n]);
print(n);
}
puts("--------------------");
}
int main(void){
// freopen("awa.in" , "r" , stdin);
int T; scanf("%d" , &T);
while(T--) solve();
}

P5503 灯塔#

显然可以翻转序列做两遍。

pi=max{hj+ijhi}p_i=\max\{h_j+\sqrt{i-j}-h_i\}

可以证明有决策单调性,用分治优化。

#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
int n , h[500005];
double p1[500005] , p2[500005];
void solve(double p[500005] , int l , int r , int ql , int qr){
int mid = (l + r) >> 1 , k = ql;
for(int i = ql , _ = min(mid - 1 , qr);i <= _;i++)
if(h[i] + __builtin_sqrt(mid - i) - h[mid] > p[mid])
p[mid] = h[i] + __builtin_sqrt(mid - i) - h[mid] , k = i;
if(l < mid) solve(p , l , mid - 1 , ql , k);
if(mid < r) solve(p , mid + 1 , r , k , qr);
}
int main(void){
scanf("%d" , &n);
for(int i = 1;i <= n;i++) scanf("%d" , &h[i]);
//p = max(h[j]+sqrt(i-j))-h[i]
solve(p1 , 1 , n , 1 , n);
reverse(h + 1 , h + 1 + n);
solve(p2 , 1 , n , 1 , n);
for(int i = 1;i <= n;i++) printf("%.0lf\n" , ceil(max(p1[i] , p2[n - i + 1])));
}

slope trick#

线段树#

凸优化#

wqs 二分#

WQS 二分通常用于解决这样一类优化问题:它们带有数量限制,直接求解代价较高;但一旦去除这一限制,问题本身就变得容易得多。

并且要求为凸函数。

一般的,可以二分选择的代价惩罚,然后根据找到一个代价,使得刚好选到要求的个数。

邮局加强版加强版#

二分放邮局的代价,用四边形不等式优化O(nlogn)O(n\log n)算出最优解,然后调整即可。

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
int n , m , a[500005]; ll sum[500005];
inline ll w(int l , int r){
int mid = (l + r) >> 1;
return 1LL * (mid - l) * a[mid] - (sum[mid - 1] - sum[l - 1]) + (sum[r] - sum[mid]) - 1LL * (r - mid) * a[mid];
}
struct ZBJ { int x , l , r; };
deque <ZBJ> q;
pair <ll , int> f[500005];
inline ll v(int x , int y){ //x -> y
return f[x].first + w(x + 1 , y);
}
inline ll solve(ll mid){
// memset(f , 0x3f , sizeof(f));
while(!q.empty()) q.pop_front();
f[0] = {0 , 0};
q.push_back((ZBJ){0 , 1 , n});
for(int i = 1;i <= n;i++){
while(!q.empty() && q.front().r < i) q.pop_front();
q.front().l = i;
f[i] = {v(q.front().x , i) + mid , f[q.front().x].second + 1};
while(!q.empty() && v(i , q.back().l) <= v(q.back().x , q.back().l))
q.pop_back();
if(q.empty()){ q.push_back((ZBJ){i , i + 1 , n}); continue; }
int l = q.back().l , r = q.back().r + 1 , ret = r;
while(l <= r){
int mi = (l + r) >> 1;
if(v(i , mi) <= v(q.back().x , mi)){
ret = mi;
r = mi - 1;
}
else l = mi + 1;
}
if(ret <= n){
q.back().r = ret - 1;
q.push_back((ZBJ){i , ret , n});
}
}
return f[n].second;
}
int main(void){
scanf("%d%d" , &n , &m);
for(int i = 1;i <= n;i++) scanf("%d" , &a[i]);
sort(a + 1 , a + 1 + n);
for(int i = 1;i <= n;i++) sum[i] = sum[i - 1] + a[i];
ll l = 0 , r = 1e12 , ret;
while(l <= r){
ll mid = (l + r) >> 1;
if(solve(mid) >= m){
ret = mid;
l = mid + 1;
}
else r = mid - 1;
}
solve(ret);
printf("%lld\n" , f[n].first - ret * m);
}

P5896 [IOI 2016] aliens#

大约可以看成线段覆盖问题。

首先按照线段ll排序,将完全被其他线段包含的去掉(显然对答案没有贡献)。

画图可知,设fif_i位覆盖前ii个线段的最小代价。

于是fi=min{fj+(rilj+1+1)2max{rjlj+1+1}2}f_i=\min \{ f_j+(r_i - l_{j+1}+1)^2-\max\{r_j-l_{j+1}+1\}^2\}

用 wqs 二分即可做到 41 分。

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
inline ll sqr(int x){ return 1LL * x * x; }
int n , m , k;
struct Line {
int l , r;
inline bool operator < (const Line b){ if(l == b.l) return r > b.r; return l < b.l; }
}qwq[1000005]; ll g[1000005];
pair <ll , int> f[1000005];
inline int solve(ll mid){
memset(f , 0x3f , sizeof(f));
f[0] = {0 , 0};
for(int i = 1;i <= n;i++)
for(int j = 0;j < i;j++)
f[i] = min(f[i] , {f[j].first - g[j] + sqr(qwq[i].r - qwq[j + 1].l + 1) + mid , f[j].second + 1});
return f[n].second;
}
int main(void){
scanf("%d%d%d" , &n , &m , &k);
if(n >= 50000){ puts("NO"); return 0; }
for(int i = 1;i <= n;i++){
scanf("%d%d" , &qwq[i].l , &qwq[i].r);
qwq[i].l++; qwq[i].r++;
if(qwq[i].l > qwq[i].r) swap(qwq[i].l , qwq[i].r);
}
sort(qwq + 1 , qwq + 1 + n);
int chm_qwq = 0;
for(int i = 1 , r = -1;i <= n;i++)
if(qwq[i].r > r){ r = qwq[i].r; qwq[++chm_qwq] = qwq[i]; }
n = chm_qwq;
for(int i = 1;i < n;i++) g[i] = sqr(max(qwq[i].r - qwq[i + 1].l + 1 , 0));
ll l = 0 , r = 1e12 , ret = -114514;
while(l <= r){
ll mid = (l + r) >> 1LL;
if(solve(mid) <= k){
ret = mid;
r = mid - 1;
}
else l = mid + 1;
}
solve(ret);
printf("%lld\n" , f[n].first - ret * k);
}

然后再对ff斜率优化即可 AC

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
inline ll sqr(int x){ return 1LL * x * x; }
int n , m , k;
struct Line {
int l , r;
inline bool operator < (const Line b){ if(l == b.l) return r > b.r; return l < b.l; }
}qwq[1000005]; ll g[1000005];
int q[1000005] , hd , tl;
pair <ll , int> f[1000005];
inline ll Y(int x){ return f[x].first - g[x] + sqr(qwq[x + 1].l); }
inline ll X(int x){ return qwq[x + 1].l; }
inline long double count(int x , int y){ return (long double)(Y(x) - Y(y)) / (X(x) - X(y)); }
inline int solve(ll mid){
q[hd = tl = 1] = 0;
for(int i = 1;i <= n;i++){
while(tl > hd && count(q[hd] , q[hd + 1]) < 2 * qwq[i].r) hd++;
const int j = q[hd];
f[i] = {f[j].first - g[j] + sqr(qwq[i].r - qwq[j + 1].l) + mid , f[j].second + 1};
while(tl > hd && count(q[tl - 1] , q[tl]) > count(q[tl] , i)) tl--;
q[++tl] = i;
}
// for(int i = 1;i <= n;i++)
// for(int j = 0;j < i;j++)
// f[i] = min(f[i] , {f[j].first - g[j] + sqr(qwq[i].r - qwq[j + 1].l + 1) + mid , f[j].second + 1});
return f[n].second;
}
int main(void){
scanf("%d%d%d" , &n , &m , &k);
for(int i = 1;i <= n;i++){
scanf("%d%d" , &qwq[i].l , &qwq[i].r);
if(qwq[i].l > qwq[i].r) swap(qwq[i].l , qwq[i].r);
}
sort(qwq + 1 , qwq + 1 + n);
int chm_qwq = 0;
for(int i = 1 , r = -1;i <= n;i++)
if(qwq[i].r > r){ r = qwq[i].r; qwq[++chm_qwq] = qwq[i]; }
n = chm_qwq;
for(int i = 1;i < n;i++) g[i] = sqr(max(qwq[i].r - qwq[i + 1].l + 1 , 0));
for(int i = 1;i <= n;i++) qwq[i].r++;
ll l = 0 , r = 1e12 , ret = -114514;
while(l <= r){
ll mid = (l + r) >> 1LL;
if(solve(mid) <= k){
ret = mid;
r = mid - 1;
}
else l = mid + 1;
}
solve(ret);
// printf("qwq%d %d\n" , ret , solve(ret));
printf("%lld\n" , f[n].first - ret * k);
}

矩阵#

技巧:边权小时可以拆点,变成边权为 1。

P6569#

模板,主要是满不满足结合律不好证。

我也不会证明。。

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
int n , m , q;
int N;
struct ZBJ {
ll a[102][102];
inline void clear(){ for(int i = 1;i <= N;i++) for(int j = 1;j <= N;j++) a[i][j] = 0; }
inline friend ZBJ operator * (const ZBJ x , const ZBJ y) {
ZBJ ret; ret.clear();
for(int i = 1;i <= N;i++) for(int j = 1;j <= N;j++)
for(int k = 1;k <= N;k++)
ret.a[i][j] ^= x.a[i][k] * y.a[k][j];
return ret;
}
}A[35];
ll f[105] , tmp[105];
inline void mul(ll k){
for(int _ = 0;_ < 32;_++) if(k >> _ & 1){
for(int i = 1;i <= N;i++) tmp[i] = f[i];
for(int i = 1;i <= N;i++) f[i] = 0;
for(int j = 1;j <= N;j++) for(int k = 1;k <= N;k++)
f[j] ^= tmp[k] * A[_].a[k][j];
// printf("%d %d %d %d %d\n" , j , k , f[j] , tmp[j] , A[_].a[k][j]);
}
}
pair <ll , int> qqq[105]; ll ans[105];
int main(void){
scanf("%d%d%d" , &n , &m , &q); N = n;
for(int i = 1;i <= n;i++) scanf("%lld" , &f[i]);
for(int i = 0;i < 32;i++) A[i].clear();
while(m--){
int u , v; scanf("%d%d" , &u , &v);
A[0].a[u][v] = A[0].a[v][u] = 1;
}
for(int i = 1;i <= q;i++) scanf("%lld" , &qqq[i].first) , qqq[i].second = i;
sort(qqq + 1 , qqq + 1 + q);
for(int i = 1;i < 32;i++) A[i] = A[i - 1] * A[i - 1];
ll lt = 0;
for(int i = 1;i <= q;i++){
mul(qqq[i].first - lt); lt = qqq[i].first;
ans[qqq[i].second] = f[1];
}
for(int i = 1;i <= q;i++) printf("%lld\n" , ans[i]);
}

P6772#

这题很容易写出 fv,j=max{fi,jw+cv}f_{v,j}=\max\{ f_{i,j-w}+c_v\},这还不是很符合(max,+)(\max,+)矩阵。

发现w5w\le 5,很小,拆边边太多了,于是拆点,每个点拆成 5 个,连边就可以连(u,w)(u,w)(v,1)(v,1)就可以了。

这样没条边的边权都为 1 了,直接放到矩阵上,点数为5n5n

但是这样无法处理节日,因此可以分段,按照时间排个序,然后按照上题的方法优化,跑完后更新。

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
#define ID(x , k) ((x) + (k - 1) * n)
const ll inf = 1e16;
int n , m , T , k; ll a[255];
int N;
struct ZBJ {
ll a[252][252];
inline void clear(){ for(int i = 1;i <= N;i++) for(int j = 1;j <= N;j++) a[i][j] = -inf; }
inline friend ZBJ operator * (const ZBJ x , const ZBJ y) {
ZBJ ret; ret.clear();
for(int i = 1;i <= N;i++) for(int j = 1;j <= N;j++)
for(int k = 1;k <= N;k++)
ret.a[i][j] = max(ret.a[i][j] , x.a[i][k] + y.a[k][j]);
return ret;
}
}A[31];
ll f[2][252];
inline void mul(int k , int nw , int lt){
for(int _ = 0;_ < 30;_++) if(k >> _ & 1){
for(int i = 1;i <= N;i++) f[nw][i] = -inf;
for(int i = 1;i <= N;i++) for(int j = 1;j <= N;j++)
f[nw][i] = max(f[nw][i] , f[lt][j] + A[_].a[j][i]);
for(int i = 1;i <= N;i++) f[lt][i] = f[nw][i];
}
}
struct SB { int t , x , y; inline bool operator < (const SB &b) { return t < b.t; } }th[205];
int main(void){
scanf("%d%d%d%d" , &n , &m , &T , &k);
N = n * 5;
for(int i = 0;i < 30;i++) A[i].clear();
for(int i = 1;i <= n;i++){
scanf("%lld" , &a[i]);
for(int j = 1;j < 5;j++) A[0].a[ID(i , j)][ID(i , j + 1)] = 0;
}
while(m--){
int u , v , w; scanf("%d%d%d" , &u , &v , &w);
A[0].a[ID(u , w)][v] = max(A[0].a[ID(u , w)][v] , a[v]);
}
for(int i = 1;i <= k;i++) scanf("%d%d%d" , &th[i].t , &th[i].x , &th[i].y);
sort(th + 1 , th + 1 + k);
if(th[k].t != T) th[++k] = (SB){T , 0 , 0};
for(int i = 1;i <= N;i++) f[0][i] = f[1][i] = -inf;
f[0][1] = a[1];
for(int i = 1;i < 30;i++) A[i] = A[i - 1] * A[i - 1];
int lt = 0;
for(int i = 1;i <= k;i++){
mul(th[i].t - lt , (i & 1) , (i & 1 ^ 1));
if(f[th[i].x] >= 0) f[i & 1][th[i].x] += th[i].y;
lt = th[i].t;
}
if(f[k & 1][1] <= 0) puts("-1");
else printf("%lld\n" , f[k & 1][1]);
}

T547970#

link

突然发现我好久之前做数学题出的题,当时觉得太简单了,现在看来是太典了。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

部分信息可能已经过时