dp 杂题笔记
注意:
本文章中的题均在蓝及以上。
原题链接若没有特殊说明,默认指向 Luogu 链接。
目前已写完的题:
- Zoltan
- Maximizing Root
线性 dp
Zoltan
题意简述:
给你一个长度为
( )的序列 ( ),从第一个数开始,把当前数放在另一个序列 ( 序列刚开始长度为 )的左边或右边,求所有可能的 序列中最长严格上升子序列的长度(设为 ),以及最长严格上升子序列长度是 的 序列个数(个数要模上 )。
注意:两个序列是相同的,当且仅当每一个数加入她们的顺序一样。例如, 和 是可能不同的。
可以发现:在组成的新序列中,一个数
举个例子(为了对齐方便,用 O
表示原序列,N
表示新序列):
O:1 2 3 4 5 6
^ - + - + +
N:6 5 3 1 2 4
+ + + ^ - -
在新序列中,我们以 ^
的数),她左边的数下标 +
、右边的数下标 -
。
再整理一下就是:
在新序列中 1 的左边的数。
O:3 5 6
N:6 5 3
在新序列中 1 的右边的数。
O:2 4
N:2 4
这下懂了吧?
But,这有什么用呢?
这玩意用处老大了!
在新序列中,我们把
举个例子(在新序列中以
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
+ + ^ - -
唉?
但怎么用这个性质?
《不为谁而作的歌》
那么对于一个新序列中最长严格上升子序列的中心
注意一下 dp 肯定是在原序列上跑的。
所以,对于原序列中的每个数
于是最长一个上升子序列的长度我们就求出来了!
接下来是处理最长严格上升子序列长度是
注意代码中的一些处理细节。
码儿:
#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
题意简述:
有
件物品,每件物品都有三个属性 、 、 。
给你个询问,每次询问给出三个数 ,问是否可以选出一些物品使得:
- 对于每个选出的物品
,满足 且 。 - 所有选出的物品的
和恰好是 。
, , 。
, , , 。
应该很明显吧?这是个背包题。
在线似乎不太好做,考虑离线。
设选出的物品的集合为
将什么东西当作“体积”加入状态?
再来考虑题目给出的这个条件:
这时候,离线高兴了。
我们可以把每个物品以
那么,不需要其他关键字了吗?
这个一会再分析。
现在就差
所以考虑以
那么“价值”越怎么样越优?
当然是越大越优,因为题目要求
于是我们设
咕咕咕
区间 dp
守卫
题意简述:
有一座山,用有
( )个折点的折线表示(折线下方全是岩石),第 个折点的坐标为 ( ),且每个折点上都有一个亭子,九条可怜只会在亭子上玩,保镖也只会在亭子处监视可怜。
保镖只能向左看,称保镖在亭子能看到亭子 ( ),当且仅当亭子 和亭子 的连线不经过任何岩石或亭子。
对于每个区间( ),算出能监视 中每个亭子所需要的保镖的最少数量,由于输出量很大,你只需要求出最少数量的异或和即可。
因为是用神威·太湖之光进行评测的。
用凸包或单调栈?
其实我都还不会用。
首先你需要能看出来这是个 dp 题。
我们需要每个区间的答案,而这个就是区间 dp 最擅干的事,并且暴力 dp 的优化空间都挺大的。
More importantly,题目中有个非常重要的信息:保镖只能向左看(这题是 dp 的主要原因)。
所以对于一个区间
因为是区间 dp,所以状态是
因为
for (int r=1;r<=n;r++) {
for (int l=r;l>=1;l--) {
for (int k=l;k<=r;k++) {
//do something . . .
}
}
}
设
那么
转移方程为:
当然,这是错的。
我不是故意的,我是有意的。
你想想啊,我们是不是忘了谁?
观察方程,怎么把
令
于是正确的暴力转移方程式为:
接下来解释
例如下图就当是图吧(O
表示亭子,附近的数字是编号):
1
O 5
\ O
\ 3 / \ 7
O--------O / \ O
2 \ / \ /
\ / \ /
O O
4 6
假设我们在处理
咕咕咕
码儿:
#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
题意简述:
给你一个有
( )个节点的树,根节点是 ,每个点有一个权值 ( )。
你可以进行以下操作(可以不操作):
选择一个之前没选过的节点,以及一个正整数 ,需要满足 是 子树内所有节点的权值的公约数,然后令 子树内所有的节点的权值都乘上一个 。
问在操作不超过( )次后 的最大可能值是多少?
似乎没有简多少。
先设置状态,节点编号是必须放进去的,但是一个状态感觉并不够,在加一个什么当状态呢?公约数或许是个不错的选择。
整理一下,就是:设
接下来是转移。
下层的操作会对上次造成影响,所以从下往上进行 dp 是非常可行的。(其实一般树形 dp 都是从下往上进行 dp,但注意只是在一般情况下)
枚举节点
但是我们不知道
因为每个点只能操作一次,这就能大大地方便我们的各项处理,因此状态上,我们强制节点
转移这时候就不难了,考虑两种情况:
- 节点
不进行操作: - 节点
进行操作:
这俩情况可以放在一个循环里处理。
但是这样时间复杂度是
优化时间又到了。
上面有这么一句话(不用往上翻了):
“当然,
所以我们没有必要从
我们设一个数
转移方程为变为(我们用
这一优化,使我们的 dp 的时间复杂度和空间复杂度都降到了我们可以接受的范围,这简直就是一举两得、一箭双雕、every nice 啊!
答案统计时间到!
由于我们强制 int
);否则是答案是
并且可以发现,
此题完~
哦,对了!注意这题有点卡常,用 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
题意简述:
给你一个有
( )个节点的树,根节点是 ,并且每个节点都有一个权值 ( )。
你要从中选出一些点,并满足以下条件:
- 一个点的父节点若未被选择,则这个点就不能被选择。
- 所选点的集合内不能有相同的权值。
- 对于每一个选择的点,其子树中所有被选择的点的权值必须可以构成公差为
的等差数列。
求满足上述条件的方案个数。
注意:这里的方案是指所选的的点的权值的集合不同的方案。
其实数据可以出得更大,
一看到树,基本上就是树形 dp 了。
公差为
这玩意将会是我们做这个题的基础。
于是我们的 brain 会很自然地想到一个暴力(不要直接不看了,我讲暴力是有目的的):
设
转移怎么搞?似乎不太好搞?
这里先咕咕一下。
码儿:
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】