dp 杂题笔记
注意:
本文章中的题均在蓝及以上。
原题链接若没有特殊说明,默认指向 Luogu 链接。
目前已写完的题:
- Zoltan
- Maximizing Root
线性 dp
Zoltan
题意简述:
给你一个长度为 \(N\)(\(N \le 2 \times 10^5\))的序列 \(A\)(\(A_i \le 10^9\)),从第一个数开始,把当前数放在另一个序列 \(B\)(\(B\) 序列刚开始长度为 \(0\))的左边或右边,求所有可能的 \(B\) 序列中最长严格上升子序列的长度(设为 \(M\)),以及最长严格上升子序列长度是 \(M\) 的 \(B\) 序列个数(个数要模上 \(10^9+7\))。
注意:两个 \(B\) 序列是相同的,当且仅当每一个数加入它们的顺序一样。例如,\([1,1]\) 和 \([1,1]\) 是可能不同的。
可以发现:在组成的新序列中,一个数 \(x\) 左边的所有的数在原序列所对应的顺序相反、右边的所有的数在原序列所对应的顺序相同。
举个例子(为了对齐方便,用 O 表示原序列,N 表示新序列):
O:1 2 3 4 5 6
^ - + - + +
N:6 5 3 1 2 4
+ + + ^ - -
在新序列中,我们以 \(1\) 为中心(下面标有 ^ 的数),它左边的数下标 +、右边的数下标 -。
再整理一下就是:
在新序列中 1 的左边的数。
O:3 5 6
N:6 5 3
在新序列中 1 的右边的数。
O:2 4
N:2 4
这下懂了吧?
But,这有什么用呢?
这玩意用处老大了!
在新序列中,我们把 \(x\) 作为中心,那么组成这个新序列的最长严格上升子序列就会被分成两半:\(x\) 左边一半,\(x\) 右边一半,可能 \(x\) 也会在最长严格上升子序列中。左边那一半对应原序列是一个严格下降子序列,右边那一半对应原序列是一个严格上升子序列。
举个例子(在新序列中以 \(5\) 为中心)。
O:5 6 3 1 4 2 7
^ - + + -
N:4 1 3 5 6 2 7
+ + ^ - -
新序列的最长严格上升子序列:1 3 5 6 7
5 左边一半:
O:3 1
N:1 3
5 右边一半:
O:6 7
N:6 7
你可能会想:能不能在原序列求个严格下降子序列和严格上升子序列,然后把它们拼成一个新序列中的最长严格上升子序列?
不错,这和正解已经很近了。
但是你一定会发现:如果它们有重复的元素怎么办?并且合并似乎也不太好搞。
把刚才上面的新序列中最长严格上升子序列拿来研究研究。
1 3 5 6 7
+ + ^ - -
唉?\(5\) 左边一半的最长严格上升子序列最大的数严格小于 \(5\) 右边一半的最长严格上升子序列最小的数。对应到原序列就是:我们得到的严格下降子序列最大的数严格小于我们得到的严格上升子序列最小的数。
但怎么用这个性质?
\(5\)(代表新序列最长严格上升子序列的中心):你是不是忘了谁?《不为谁而作的歌》
那么对于一个新序列中最长严格上升子序列的中心 \(x\),以它为起点向外扩展的最长严格下降子序列和最长严格上升子序列是一定没有重复元素的(除了 \(x\),不过这个好处理)。
注意一下 dp 肯定是在原序列上跑的。
所以,对于原序列中的每个数 \(x\),把它强制作为新序列中最长严格上升子序列的中心,令 \(f_{i,0}\)、\(cnt_{i,0}\) 分别表示以 \(i\) 为结尾的最长严格上升子序列长度以及方案数,再令 \(f_{i,1}\)、\(cnt_{i,1}\) 分别表示以 \(i\) 为结尾的最长严格下降子序列以及方案数,那么在所有可能的新序列中最长严格上升子序列的长度 \(M\) 就是 \(f_{i,0}+f_{i,1}-1\),减 \(1\) 是因为 \(x\) 是它们唯一一个重复的数。
于是最长一个上升子序列的长度我们就求出来了!
接下来是处理最长严格上升子序列长度是 \(M\) 的 \(B\) 序列的个数。先讨论有多少个新序列能够有同一个固定的最长严格上升子序列。由于组成最长严格上升子序列的数的放左或放右都已经定好了,所以它们对答案没什么影响,但是其他不组成这个最长严格上升子序列的数就不一样了,它们放左或放右都行,所以有 \(2^{N-M}\) 个新序列能够有同一个固定的最长严格上升子序列。不过最长严格上升子序列可能不止一个,其个数为 \(\sum\limits_{i=1}^n cnt_{i,0} \times cnt_{i,1}[f_{i,0}+f_{i,1}-1=M]\),答案就是它们俩的积,即
注意代码中的一些处理细节。
码儿:
#include<bits/stdc++.h>
#define f first
#define s second
using namespace std;
typedef pair<int,int> PII;
const int N=2e5+5,P=1e9+7;
int n,m,len,ans;
int a[N],b[N],f[N][2],cnt[N][2];
struct TreeArray{ //这是树状数组,用来进行 dp。
PII c[N];
int lowbit(int x) {return x&-x;}
void add(int x,int p,int s) {
for (int i=x;i<=m;i+=lowbit(i)) {
if (p>c[i].f) {c[i]={p,s};}
else if (p==c[i].f) {(c[i].s+=s)%=P;}
}
}
PII query(int x) {
PII res={0,0};
for (int i=x;i;i-=lowbit(i)) {
if (res.f<c[i].f) {res=c[i];}
else if (res.f==c[i].f) {(res.s+=c[i].s)%=P;}
}
res.s=max(res.s,1); //注意个数最少是 1,不可能没有。
return res;
}
}c0,c1;
inline int read() { //快读,没啥好看的。
int x=0,f=1;
char c=getchar();
while (!isdigit(c)) {f=(c=='-'?-1:1);c=getchar();}
while (isdigit(c)) {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int main() {
n=read();
for (int i=n;i;i--) {a[i]=b[i]=read();}
sort(b+1,b+1+n);
m=unique(b+1,b+1+n)-b-1;
for (int i=1;i<=n;i++) { //离散化。
a[i]=lower_bound(b+1,b+1+m,a[i])-b;
}
/*----------------------一整个程序的精华----------------------*/
for (int i=1;i<=n;i++) { //dp 部分。
PII t0=c0.query(a[i]-1);
PII t1=c1.query(m-a[i]);
f[i][0]=t0.f+1;cnt[i][0]=t0.s;
f[i][1]=t1.f+1;cnt[i][1]=t1.s;
c0.add(a[i],f[i][0],cnt[i][0]);
c1.add(m-a[i]+1,f[i][1],cnt[i][1]);
}
for (int i=1;i<=n;i++) { //求个数。
int L=f[i][0]+f[i][1]-1;
if (L>len) {len=L;ans=1ll*cnt[i][0]*cnt[i][1]%P;}
else if (L==len) {ans=(ans+1ll*cnt[i][0]*cnt[i][1]%P)%P;}
}
for (int i=1;i<=n-len;i++) {ans=2ll*ans%P;}
/*--------------------------------------------------------*/
printf("%d %d\n",len,ans);
return 0;
}
背包
SZA-Cloakroom
题意简述:
有 \(n\) 件物品,每件物品都有三个属性 \(c_i\)、\(a_i\)、\(b_i\) 。
给你 \(q\) 个询问,每次询问给出三个数 \(m_i,k_i,s_i\),问是否可以选出一些物品使得:
- 对于每个选出的物品 \(i\),满足 \(a_i \le m_i\) 且 \(b_i > m_i + s_i\)。
- 所有选出的物品的 \(c_i\) 和恰好是 \(k_i\)。
\(1\le n\le 1000\),\(1\le a_i<b_i\le 10^9\),\(1\le c_i\le 1000\)。
\(1\le q\le 10^6\),\(1\le m_i\le 10^9\),\(1\le k_i\le 10^5\),\(0\le s_i\le 10^9\)。
应该很明显吧?这是个背包题。
在线似乎不太好做,考虑离线。
设选出的物品的集合为 \(S\)。
将什么东西当作“体积”加入状态?
\(a_i,b_i\) 范围太 big 了,\(\sum\limits_{i=1}^n c_i\) 最大才 \(10^6\),并且以 \(\sum\limits_{i\in S}c_i\) 为“体积”似乎挺方便转移与统计答案的,于是我们暂定 \(\sum\limits_{i\in S}\) 作为“体积”。
再来考虑题目给出的这个条件:\(a_i\le m\)。
这时候,离线高兴了。
我们可以把每个物品以 \(a_i\) 为关键字升序排序,再把每个询问以 \(m_i\) 为关键字升序排序,跑个双指针,就能保证 \(a_i\le m_i\) 了。
那么,不需要其他关键字了吗?
这个一会再分析。
现在就差 \(b_i\) 了,并且每个物品的“价值”还没定。
所以考虑以 \(b_i\) 为“价值”。
那么“价值”越怎么样越优?
当然是越大越优,因为题目要求 \(b_i>m_i+s_i\)。
于是我们设 \(f_i\) 表示 \(\sum\limits_{j\in S}c_j=i\) 时最大的 \(\min\limits_{j\in S}(\{b_j\})\)。
咕咕咕
区间 dp
守卫
题意简述:
有一座山,用有 \(n\)(\(n \le 5000\))个折点的折线表示(折线下方全是岩石),第 \(i\) 个折点的坐标为 \((i,h_i)\)(\(1 \le h_i \le 10^9\)),且每个折点上都有一个亭子,九条可怜只会在亭子上玩,保镖也只会在亭子处监视可怜。
保镖只能向左看,称保镖在亭子 \(p\) 能看到亭子 \(q\)(\(1 \le q \le p \le n\)),当且仅当亭子 \(p\) 和亭子 \(q\) 的连线不经过任何岩石或亭子。
对于每个区间 \([l,r]\)(\(1 \le l \le r \le n\)),算出能监视 \([l,r]\) 中每个亭子所需要的保镖的最少数量,由于输出量很大,你只需要求出最少数量的异或和即可。
\(n \le 5000\)?这跟区间 dp 有什么关系?\(\mathrm{O}(n^3)\) 是不妥妥 TLE 到飞起吗?
因为是用神威·太湖之光进行评测的。
用凸包或单调栈?
其实我都还不会用。
首先你需要能看出来这是个 dp 题。
我们需要每个区间的答案,而这个就是区间 dp 最擅干的事,并且暴力 dp 的优化空间都挺大的。
More importantly,题目中有个非常重要的信息:保镖只能向左看(这题是 dp 的主要原因)。
所以对于一个区间 \([l,r]\),亭子 \(r\) 上必须有一个保镖,不然就没有保镖能看见亭子 \(r\) 了。
因为是区间 dp,所以状态是 \(f_{l,r}\),不用减状态或加状态。
因为 \(r\) 的特殊地位,所以我们采用这种区间 dp 方式:
for (int r=1;r<=n;r++) {
for (int l=r;l>=1;l--) {
for (int k=l;k<=r;k++) {
//do something . . .
}
}
}
设 \(p_1,p_2,\cdots,p_{t-1},p_t\)(\(l\le p_1<p_2<\cdots<p_{t-1}<p_t=r\))表示亭子 \(r\) 在区间 \([l,r]\) 中能够看到的亭子。
那么 \([p_i+1,p_{i+1}-1]\) 这个区间上所有的亭子是亭子 \(r\) 看不到的,需要再放置保镖。首先要考虑一下这个区间的右端点怎么定。你可能会疑惑:右端点直接定 \(p_{i+1}-1\) 不就行了吗?其实,\(p_{i+1}\) 也可能成为右端点!
转移方程为:
当然,这是错的。
我不是故意的,我是有意的。
你想想啊,我们是不是忘了谁?
\(l\):Do you remember me?
观察方程,怎么把 \(l\) 加进去呢?
令 \(p_0=l-1\) 即可,\(k\) 要从 \(0\) 开始。
于是正确的暴力转移方程式为:
接下来解释 \(p_{i+1}-1\) 为什么能作右端点。
例如下图就当是图吧(O 表示亭子,附近的数字是编号):
1
O 5
\ O
\ 3 / \ 7
O--------O / \ O
2 \ / \ /
\ / \ /
O O
4 6
假设我们在处理 \([1,7]\) 区间,\(p\) 你一眼就求出来了,是。
咕咕咕
码儿:
#include<bits/stdc++.h>
using namespace std;
const int N=5005;
int n,ans;
int h[N],f[N][N];
inline int read() {
int x=0,f=1;
char c=getchar();
while (!isdigit(c)) {f=(c=='-'?-1:1);c=getchar();}
while (isdigit(c)) {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
double calc(int x,int y) {
return 1.0*(h[x]-h[y])/(x-y);
}
int main() {
n=read();
for (int i=1;i<=n;i++) {h[i]=read();}
for (int r=1;r<=n;r++) {
ans^=(f[r][r]=1);
for (int l=r-1,i=0;l;l--) {
if (!i || calc(l,r)<calc(i,r)) {i=l;}
ans^=(f[l][r]=min(f[l][i],f[l][i-1])+f[i+1][r]);
}
}
printf("%d\n",ans);
return 0;
}
树形 dp
Maximizing Root
题意简述:
给你一个有 \(n\)(\(2 \le n \le 10^5\))个节点的树,根节点是 \(1\),每个点有一个权值 \(a_i\)(\(1 \le a_i \le 1000\))。
你可以进行以下操作(可以不操作):
选择一个之前没选过的节点 \(u\),以及一个正整数 \(x\),需要满足 \(x\) 是 \(u\) 子树内所有节点的权值的公约数,然后令 \(u\) 子树内所有的节点的权值都乘上一个 \(x\)。
问在操作不超过 \(m\)(\(0 \le m \le n\))次后 \(a_1\) 的最大可能值是多少?
似乎没有简多少。
先设置状态,节点编号是必须放进去的,但是一个状态感觉并不够,在加一个什么当状态呢?公约数或许是个不错的选择。
整理一下,就是:设 \(f_{u,w}\) 表示以 \(u\) 为根节点的子树中,使其所有节点的权值的公约数变为 \(w\) 的最小操作次数。
接下来是转移。
下层的操作会对上次造成影响,所以从下往上进行 dp 是非常可行的。(其实一般树形 dp 都是从下往上进行 dp,但注意只是在一般情况下)
枚举节点 \(u\) 的每一个子节点 \(v\),然后枚举以 \(u\) 为根的子树中所有节点权值可能的公约数 \(wu\),当然也要枚举一个 \(wv\) 表示以 \(v\) 为根的子树中所有节点可能的公约数。当然,\(wu\) 必须满足是 \(a_u\) 的约数,\(wv\) 必须是 \(a_v\) 的约数。
但是我们不知道 \(u\) 或 \(v\) 是否操作过,还需要加一维吗?其实,这不——需——要。
因为每个点只能操作一次,这就能大大地方便我们的各项处理,因此状态上,我们强制节点 \(u\) 不进行操作。
转移这时候就不难了,考虑两种情况:
- 节点 \(v\) 不进行操作:\(f_{u,wu}=\min(\{ \begin{cases} f_{v,wv} & wv \bmod wu = 0\\ +\infty & wv \bmod wu \ne 0\\ \end{cases}\}) \)
- 节点 \(v\) 进行操作:\(\ \ \ f_{u,wu}=\min(\{ \begin{cases} f_{v,wv}+1 & wv^2 \bmod wu = 0\\ +\infty & wv^2 \bmod wu \ne 0\\ \end{cases} \})\)
这俩情况可以放在一个循环里处理。
但是这样时间复杂度是 \(\mathrm{O}(nV^2)\)(\(V\) 表示 \(a_i\) 的值域)的,直接 TLE 到飞起,并且空间复杂度也不容乐观。
优化时间又到了。
上面有这么一句话(不用往上翻了):
“当然,\(wu\) 必须满足是 \(a_u\) 的约数,\(wv\) 必须是 \(a_v\) 的约数。”
所以我们没有必要从 \(1\) 枚举到 \(wu\) 或 \(wv\),可以预先处理处理一下 \(1 \sim 1000\) 中每个数的约数,到时候直接枚举 \(a_u\) 或 \(a_v\) 的约数即可,时间复杂度直接降到 \(\mathrm{O}(nd^2)\)(\(d\) 表示 \(a_i\) 的约数个数,其最大值为 \(32\)),空间也没有必要浪费给不是约数的数了,\(f_{u,w}\) 被重新定义为 在以节点 \(u\) 为根的子树中,使其所有节点的权值的公约数是 \(a_u\) 的第 \(w\) 个约数(约数最好是升序的)的最少操作次数,空间复杂度猛降到 \(\mathrm{O}(nd)\)。
我们设一个数 \(x\) 的第 \(i\) 个约数是 \(d_{x,i}\)。
\(wu\) 的意思换成目前枚举到的 \(a_u\) 的约数是第几个,\(wv\) 的意思换成目前枚举到的 \(a_v\) 的约数是第几个。
转移方程为变为(我们用 \(d(x)\) 表示 \(x\) 的约数个数):
这一优化,使我们的 dp 的时间复杂度和空间复杂度都降到了我们可以接受的范围,这简直就是一举两得、一箭双雕、every nice 啊!
答案统计时间到!
由于我们强制 \(f_{u,w}\) 中的节点 \(u\) 不进行操作,所以我们需要用一次操作来让 \(a_1\) 变大,因此我们从大到小枚举一个 \(w\),那么在第一次 \(f_{1,w} < m\) 时,答案就是 \(a_1 \times d_{a_1,w}\)(注意可能爆 int);否则是答案是 \(a_1\)。即使 \(f_{1,w} = m\) 也不行,因为没有操作可以给节点 \(1\) 用了。
并且可以发现,\(f\) 如果已经超过了 \(m\),再大也没意义了,所以可以把大于 \(m\) 的 \(f\) 赋成 \(m+1\)。
此题完~
哦,对了!注意这题有点卡常,用 memset 清空数组会超时。
码儿:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=1005,K=35;
int T,n,m;
int a[N],f[N][K];
int h[N],e[N<<1],ne[N<<1],idx=1;
vector<int> d[M];
inline int read() { //快读。
int x=0,f=1;
char c=getchar();
while (!isdigit(c)) {f=(c=='-'?-1:1);c=getchar();}
while (isdigit(c)) {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
namespace lyas145{
//这个 namespace 不用管,“lyas145::函数名”就能从外面调用这里面的函数。
void add(int a,int b) { //链式前向星。
e[idx]=b;ne[idx]=h[a];h[a]=idx++;
}
void clear() { //清空。
for (int i=1;i<=n;i++) {
h[i]=0;
for (int j=0;j<K;j++) {f[i][j]=0;}
}
idx=1;
}
void dfs(int u,int fa) { //树形 dp。
for (int i=h[u];i;i=ne[i]) {
int v=e[i];
if (v==fa) {continue;}
dfs(v,u);
for (int j=0;j<d[a[u]].size();j++) {
int wu=d[a[u]][j];
int res=m+1;
for (int k=0;k<d[a[v]].size();k++) {
int wv=d[a[v]][k];
if (wv%wu==0) {res=min(res,f[v][k]);}
//节点 v 不操作。
else if (wv*wv%wu==0) {res=min(res,f[v][k]+1);}
//节点 v 操作。
}
f[u][j]=min(f[u][j]+res,m+1);
}
}
}
void main() {
n=read();m=read();
for (int i=1;i<=n;i++) {a[i]=read();}
for (int i=1;i<n;i++) {
int a=read(),b=read();
add(a,b);
add(b,a);
}
dfs(1,0);
for (int i=d[a[1]].size()-1;~i;i--) {
if (f[1][i]<m) {printf("%lld\n",1ll*a[1]*d[a[1]][i]);return ;}
}
//能对节点 1 进行操作来对答案造成贡献。
printf("%d\n",a[1]);
//不能对节点 1 进行操作来对答案造成贡献。
return ;
}
}
int main() {
for (int i=1;i<=M-5;i++) {//求 1~1000 的约数。
for (int j=i;j<=M-5;j+=i) {
d[j].push_back(i);
}
}
T=read();
while (T--) {
lyas145::main();
lyas145::clear();
}
return 0;
}
UZASTOPNI
题意简述:
给你一个有 \(n\)(\(1 \le n \le 10^4\))个节点的树,根节点是 \(1\),并且每个节点都有一个权值 \(v_i\)(\(1 \le v_i \le 100\))。
你要从中选出一些点,并满足以下条件:
- 一个点的父节点若未被选择,则这个点就不能被选择。
- 所选点的集合内不能有相同的权值。
- 对于每一个选择的点,其子树中所有被选择的点的权值必须可以构成公差为 \(1\) 的等差数列。
求满足上述条件的方案个数。
注意:这里的方案是指所选的的点的权值的集合不同的方案。
其实数据可以出得更大,\(n\) 可以到 \(10^5\),\(v_i\) 能到 \(2000\)。不过这不重要,重要的是学会这题 dp 最 NB 的优化。
一看到树,基本上就是树形 dp 了。
公差为 \(1\) 的等差数列?不就是一串连续的整数吗?形式为 \([l,l+1,\cdots,r-1,r]\)。
这玩意将会是我们做这个题的基础。
于是我们的 brain 会很自然地想到一个暴力(不要直接不看了,我讲暴力是有目的的):
设 \(f_{u,l,r}\) 表示以 \(u\) 为根节点的子树中,拼出 \([l,r]\) 这个区间的方案数。
转移怎么搞?似乎不太好搞?
这里先咕咕一下。
码儿:
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5,M=105;
int n;
int a[N];
vector<int> g[N];
bitset<M> l[N],r[N];
inline int read() {
int x=0,f=1;
char c=getchar();
while (!isdigit(c)) {f=(c=='-'?-1:1);c=getchar();}
while (isdigit(c)) {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
bool cmp(int x,int y) {
return a[x]<a[y];
}
void dfs(int u,int fa) {
l[u].set(a[u]);
r[u].set(a[u]);
for (int v : g[u]) {
if (v!=fa) {dfs(v,u);}
}
for (int i=0;i<g[u].size();i++) {
int v=g[u][i];
if (a[u]<a[v] && (r[u]<<1&l[v]).any()) {r[u]|=r[v];}
}
for (int i=g[u].size()-1;~i;i--) {
int v=g[u][i];
if (a[v]<a[u] && (l[u]>>1&r[v]).any()) {l[u]|=l[v];}
}
}
int main() {
n=read();
for (int i=1;i<=n;i++) {a[i]=read();}
for (int i=1;i<n;i++) {
int a=read(),b=read();
g[a].push_back(b);
g[b].push_back(a);
}
for (int i=1;i<=n;i++) {
sort(g[i].begin(),g[i].end(),cmp);
}
dfs(1,0);
printf("%d\n",l[1].count()*r[1].count());
return 0;
}

浙公网安备 33010602011771号