小编带你来了解下 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
结构
线性
区间
背包
树 & 图
树形 dp
P2279 消防局的设立
方法一
设 为以为子树,保证能点亮 下两层/下一层/自己/父亲/上二 及以下的所有节点 的最小代价。
转移比较简单。
方法二
设为以为子树,存在的 下下层儿子/儿子/自己/父亲/往上距离为 2 的点将其点亮的最小代价。
转移读者自行尝试,如果不会,可以去问连神,chm_qwq。
虽然不知道和方法一有什么区别。
方法三
贪心,我们考虑在刚好能点亮的位置点灯。下面设为一个灯最远能点亮的距离。
于是设为以为子树最远没有被点亮的距离;为以为子树最近被点亮的点的距离。
根据定义有:,。
于是分三种情况。
- :可以直接点亮,于是。
- 如果这个点为必须点亮而没有被点亮,既,那么既要求其他点给这个点点亮。
- 若到最远距离了,直接在该点放灯塔,那么。
ex方法一/二
若在点放灯要代价。
为以为子树,向上至少能点亮层的最小代价。
为以为子树,向下至少能点亮层的最小代价。
转移读者自推不难。
状压
技巧
枚举子集
for(int T = (S - 1) & S;T;T = (T - 1) & S){ //真子集要从S开始。}高维前缀和
子集
eg:表示有几个满足 。
维护有三种理解。
去重法
最简单的想法是去掉一个转移:
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法
为了去重,我们设表示只允许右位可与不同(但是仍为的子集)的和。
将滚掉即可得到代码。
类比法
看三维前缀和:
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];若扩展到维也是如此。
假如每维只有呢,那么就可以状压成,然后枚举每一位即可。
超集
表示有几个满足 。
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 宝藏
好题。核心技巧是枚举子集,妙处在状态设计。
因为加路径需要到起点的距离,那么我们可以看成一滴水从某个顶点开始散开,一次性添加需要的同一层的所有点。
于是我们先处理出表示当前点集为,扩展的点集为的最小路径长度和。
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); }}然后求出表示共有层,点集为,显然。
#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 花园
首先可以得:为前个花后个为的方案,转移显然。环可以看成有朵花,前后相同。
然后发现均有前一状态转移而来,并且可转移的点相同,那么可以写成矩阵,矩阵快速幂优化即可。
#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] 最大前缀和
难点在转换。
考虑枚举可能的最大前缀和,显然有种,对于每种合法的最大前缀和(前个为最大):
- 满足。
- 满足。
原因显然。发现前后并不冲突,于是可以分开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:要求或起来为全集的方案数,不妨取反,于是变成求与起来为空集的方案数。
先求出表示有多少个数有。
然后表示与起来为超集的方案数,显然为。
然后差分回去就可以求出与起来为的方案数了。
#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 的数的与是当前枚举的超集。
这个可以用高维前缀和预处理。
表示为超集的下标最大值与次大值。
#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
直接按照下标转移的暂时不说。
插入法
我们按照值从小到大插入,即表示插入了小于等于的所有数的答案。
AT_dp_t Permutation
表示前个,前一个插的排名为,转移不难。
连续段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。
有以下三种情况。

好像不对。。。
P5999 [CEOI 2016] kangaroo
求波浪型排列(其差分正负交错,没有连续的正或负),满足的方案数。
显然可以从小往大插,设表示插入了前个数,形成了个段的方案数。
我们枚举。
- 新建段:
对于,它有个空位,当我们放入时,显然比之前放的都大,这里没啥限制,都是合法的。
唯一的限制是,所以当时,被放了,空位减,尾同理。
所以。
- 段添加:
我们不能允许这种操作,因为如果允许了,因为只能往块的头尾加,这样一定不满足波浪型限制,不必转移。
- 段合并:
无论放哪,两侧相邻的都比小,合成的新的块仍满足此性质。
。
答案即为。
#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
设加入了前个点,一共有个连续段。
- 新建:。
- 系数:有个空位。
- 为什么能加:不知道
优化
单调队列
决策单调性
https://www.cnblogs.com/alex-wei/p/DP_optimization_method_II.html
https://www.cnblogs.com/birchtree/p/12937975.html
四边形不等式
定义1:若对于,满足。则称满足四边形不等式。
下面的推论比较好用。
推论1.1:对于,满足则满足四边形不等式。
证明:设可得两式,相加可证,同样可证出
,所以满足四边形不等式。
推论1.2:若满足四边形不等式,那么满足凸完全单调性,即是凸函数。
从四边形不等式到决策单调性
定义2:对于的状态转移方程,若时可让取得最小值,则为的最优决策点。若单调不降,那么称具有决策单调性。
定理2.1:若满足四边形不等式,那么具有决策单调性。
证明:
,根据的定义得:f_{p_i}+w(p_i,i)\le f_j+w(j,i)\tag 1
,此时,所以
移项得:w(p_i,i')-w(p_i,i)\le w(j,i')-w(j,i) \tag 2
得:
所以的一定没有决策点优,于是的最优决策点,所以具有决策单调性。
大胆假设
一般题目可以通过枚举(瞎猜)来验证四边形不等式,无需严格证明。
然后你就拥有了一个很强的性质:决策单调性。于是你可以使用以下几种方式优化成
以分治
适用范围:转移不与当前层的有关。
比较简单的想法,由于决策单调性,所以对于可能的决策点是一个区间。
可以利用分治先求出再求出左右区间的决策点范围。
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);}以二分队列
适用范围:决策点单调不降。
考虑每个点可能作为最优决策点的范围,容易证明一定成区间。
我们可以用表示决策点可能作为的的最优决策点,放进队列里。
然后分三步:
- 无法成为的最优决策点的出队列。
- 更新。
- 出队。
- 加入队尾严格劣于,那么这个决策点一定不优。这里可以通过对的贡献大小判断。
- 空队列,直接加。
- 如果队尾部分优于,那么需要修改队尾的区间。由于区间连续,则一定存在一点满足左边队尾优,右边优,那么可以二分找出来。
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}); }}以二分栈
斜率优化
一些只和有关的项和一个和都有关的项的和,一般可以使用斜率优化。
例如:
假如一定,可以看成关于的函数。
观察,得:
现在已知所有的,要求出最大的,因为是常数,所以相当于找最大的使得直线能经过一个点。
尝天下鲜
P1912 小G诗人
设
所以
猜测满足四边形不等式,于是具有决策单调性。
所以直接用二分队列优化就行了。
#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 灯塔
显然可以翻转序列做两遍。
可以证明有决策单调性,用分治优化。
#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 二分通常用于解决这样一类优化问题:它们带有数量限制,直接求解代价较高;但一旦去除这一限制,问题本身就变得容易得多。
并且要求为凸函数。
一般的,可以二分选择的代价惩罚,然后根据找到一个代价,使得刚好选到要求的个数。
邮局加强版加强版
二分放邮局的代价,用四边形不等式优化算出最优解,然后调整即可。
#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
大约可以看成线段覆盖问题。
首先按照线段排序,将完全被其他线段包含的去掉(显然对答案没有贡献)。
画图可知,设位覆盖前个线段的最小代价。
于是。
用 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);}然后再对斜率优化即可 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
这题很容易写出 ,这还不是很符合矩阵。
发现,很小,拆边边太多了,于是拆点,每个点拆成 5 个,连边就可以连和就可以了。
这样没条边的边权都为 1 了,直接放到矩阵上,点数为。
但是这样无法处理节日,因此可以分段,按照时间排个序,然后按照上题的方法优化,跑完后更新。
#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
突然发现我好久之前做数学题出的题,当时觉得太简单了,现在看来是太典了。
部分信息可能已经过时