2026.10.1 考试
DP专题训练1
点击查看题面
最后一题时限 3s,其余题目时限 1s;所有题目内存限制 256M。
碰杯(toasting.in/toasting.out)
今天的酒席有 \(n\) 个人,他们要同时举杯,成对碰杯。碰杯的时候,不能有人不参与碰杯,也不希望有手臂交叉这种别扭的情况出现。如下图,左图的情况是好的,右图的情况是不希望出现的。

每个人都有一个喜爱的酒种类,每个人想要与和自己喝一样酒的人碰杯,请你设计一个方法,在保证每个人参与碰杯,且没有手臂交叉的情况下,有最多的人与喝一样酒的人碰杯,输出最多有多少人能与喝一样酒的人碰杯。
输入格式
第一行一个数 \(n\),表示酒桌上人的个数。
第二行 \(n\) 个数 \(c_i\),逆时针方向依次表示坐在第 \(i\) 个位置的人喝哪种酒,第 \(1\) 个和第 \(n\) 个人是相邻的。
输出格式
一个整数,表示在保证每个人参与碰杯,且没有手臂交叉的情况下,最多有多少人能与喝一样酒的人碰杯。
输入样例1
6
1 2 2 1 3 3
输出样例1
3
数据规模
- 10% 数据满足 \(2 \le n \le 10\)
- 100% 数据满足 \(2 \le n \le 1000\),\(1 \le c_i \le 100\)
分草莓(strawberry.in/strawberry.out)
院子里有一颗又高又大的草莓树,草莓树有 \(n\) 个节点,每个节点都结了一个草莓,吃掉第 \(i\) 个结点的草莓可以得到 \(a_i\) 的营养值,由于草莓可能会坏掉,所以 \(a_i\) 可能为负值,也可能为 \(0\)。
现在要砍掉这颗树的两条边,使树变成三份,并且使得三份各自草莓营养值的和恰好一样。请问是否有这样的方法呢?如果有,请输出 YES,否则输出 NO。
输入格式
第一行一个数 \(t\),表示测试点的个数。
接下来 \(t\) 组:
每组第一行一个数 \(n\),表示结点的个数。
接下来 \(n\) 行,每行两个数 \(fa\) 和 \(a_i\),表示第 \(i\) 个结点的父亲是 \(fa\),第 \(i\) 个结点有营养值为 \(a_i\) 的草莓,根节点的 \(fa\) 记为 \(0\)。
输出格式
输出 \(t\) 行,如果第 \(i\) 组有解,则输出 YES,否则输出 NO。
输入样例
2
6
2 4
0 5
4 2
2 1
1 1
4 2
6
2 4
0 6
4 2
2 1
1 1
4 2
输出样例
YES
NO
样例解释
第一组可以切掉 \(1\) 号和 \(4\) 号的父边,形成均等的三份。
数据规模
- 10% 数据满足 \(3 \le n \le 100\)
- 100% 数据满足 \(t \le 20\),\(3 \le n \le 100000\),\(-100 \le a_i \le 100\)
深意(meaning.in/meaning.out)
有的词语常常别有深意,比如 hehe 可以单纯地表示 hehe,也可以表示“excuse me?!”。现给出一段话 \(s\),和一个单词 \(t\),已知单词 \(t\) 有别有深意,也就是说有两个意思,求 \(s\) 可能有多少个意思?答案对 \(1000000007\) 取模。
输入格式
第一行一个数 \(n\),表示测试的组数。
接下来 \(n\) 组,每组两行,表示 \(s\) 和 \(t\)。
输出格式
一共 \(n\) 行,第 \(i\) 行表示第 \(i\) 组的 \(s\) 个意思个数。
输入样例
4
hehehe
hehe
woquxizaolehehe
woquxizaole
hehehehe
hehe
woyaogugenbierenliaotianle
wanan
输出样例
3
2
5
1
数据规模
- 50% 数据满足 \(\mathrm{len}(s), \mathrm{len}(t) \le 1000\)
- 100% 数据满足 \(\mathrm{len}(t) \le \mathrm{len}(s) \le 100000\),\(n \le 10\)。\(s\) 和 \(t\) 仅包含小写字母。
斐波那契子序列(fibonacci.in/fibonacci.out)
斐波那契序列是这样一个序列 \(\{A_n\}\),对于 \(A_i = A_{i-2} + A_{i-1}\)。现在给出一个序列 \(\{C_n\}\),求其中的最长斐波那契子序列。
输入格式
第一行一个数 \(n\),表示序列的长度。
第二行 \(n\) 个数,第 \(i\) 个数表示 \(C_i\)。
输出格式
一个整数,表示最长斐波那契子序列长度。
输入样例
10
1 1 3 -1 2 0 5 -1 -1 8
输出样例
5
样例解释
1 -1 0 -1 -1 为可行的子序列。
数据规模
- 10% 数据满足 \(n \le 100\)
- 100% 数据满足 \(n \le 3000\),\(\mathrm{abs}(C_i) \le 10^9\)
总结
\(T1:0~~~~T2:0~~~~T3:100~~~~T4:100\)
T1是区间DP,不会写;
T2是树形DP,不会写;
T3非正解(KMP),由于数据较水,暴力DP即可AC;
T4先想到了\(O(n^3)\)做法,但是肯定TLE。后面在oiwiki现学了一下哈希,优化到\(O(n^2)\),拿到AC。
总之,对于DP的掌握程度还不够,仍有许多DP知识已经遗忘或者根本没学,需要下来学习
题解
T1
区间DP板子题改,很简单
问题转化
我们可以把圆桌从某处“断开”成一条链。因为圆上不交叉的弦在断开后不会跨越断点,所以直接按输入顺序 \(1,2,\dots,n\) 作为链,用区间 DP 处理即可。
状态定义
设 \(f_{i,j}\) 表示:
在按逆时针顺序排列的区间 \([i, j]\) 内(包含 \(i\) 和 \(j\)),所有人两两碰杯且手臂不交叉时,最多能形成的“同酒碰杯对”的数量。
区间长度 \(len = j - i + 1\) 必须是偶数,否则无法让区间内所有人都配对,所以我们只计算长度为偶数的区间。
状态转移
对于区间 \([i, j]\),有两种配对方式:
Case 1:\(i\) 和 \(j\) 碰杯
此时 \(i\) 与 \(j\) 组成一对,剩下的人组成区间 \([i+1, j-1]\) 继续配对。
因为整个区间长度是偶数,所以 \(j - i + 1\) 是偶数,\(j - i\) 是奇数,那么 \([i+1, j-1]\) 的长度为 \((j-1) - (i+1) + 1 = j - i - 1\),是偶数,可以完全配对。
贡献为:
其中 \(a_i\) 表示第 \(i\) 个人喝的酒种类。
Case 2:\(i\) 和 \(j\) 不碰杯
那么区间内一定存在一个分界点 \(k\),使得配对完全发生在左半部分 \([i, k]\) 和右半部分 \([k+1, j]\) 内部,两部分互不交叉。
为了保证两个子区间长度都是偶数(能完全配对),\(k\) 必须满足:
- \([i, k]\) 长度为偶数 \(\Rightarrow k - i + 1\) 为偶数 \(\Rightarrow k - i\) 为奇数。
- 因此 \(k\) 从 \(i+1\) 开始,每次增加 2。
转移:
综合两种情况,取最大值:
顺序和边界
- 区间长度 \(len\) 从 2 到 \(n\),每次增加 2(只处理偶数长度)。
- 对于每个 \(len\),枚举左端点 \(i\),右端点 \(j = i + len - 1\)。
- 因为转移需要用到更短区间的 \(f\) 值,所以按长度从小到大计算是正确的。
- 初始时所有 \(f\) 值为 0。
- 当 \(len=2\) 时,\(j = i+1\),情况 1 中 \(f_{i+1}{1}\) 是空区间,值为 0,所以 \(f_{i,i+1}=(a_i == a_{i+1}?1:0)\),符合预期。
\(AC~Code:\)
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1005;
const int INF=1e9;
int n,c[MAXN],f[MAXN][MAXN];
int main(){
freopen("toasting.in","r",stdin);
freopen("toasting.out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n;
if(n&1){
cout<<0<<'\n';
return 0;
}
for(int i=1;i<=n;i++){
cin>>c[i];
c[i+n]=c[i];
}
for(int len=2;len<=n;len+=2){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
f[i][j]=f[i+1][j-1]+(int)(c[i]==c[j]);
for(int k=i+1;k<j;k+=2){
f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]);
}
}
}
cout<<f[1][n]<<'\n';
return 0;
}
T2
//TODO
T3
暴力DP
设\(n=s.size,m=t.size\),令\(f_i\)表示字符串\(s\)前\(i\)项有针对于字符串\(t\)的分解方式,则有以下状态转移方程:
则\(f_0=1\),那么我们可以从小到大枚举\(i\),当\(i\ge m\)后可以从枚举的前\(i\)项中再枚举长度为\(m\)的字符串,与\(t\)比较,如果相等,则有以下状态转移方程:
由于题目要求对答案取模,在计算时加上%MOD即可
\(AC~Code:\)
#include<bits/stdc++.h>
using namespace std;
const int MAXN=100005;
const int MOD=1000000007;
int f[MAXN];
int main() {
freopen("meaning.in","r",stdin);
freopen("meaning.out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(0);
int T;
cin>>T;
while(T--) {
memset(f,0,sizeof(f));
string s,t;
cin>>s>>t;
int n=(int)s.size();
int m=(int)t.size();
f[0]=1;
for(int i=1;i<=n;i++){
f[i]=f[i-1];
if(i>=m&&s.substr(i-m,m)==t){
f[i]=(f[i-m]+f[i])%MOD;
}
}
cout<<f[n]<<'\n';
}
return 0;
}
正解
//TODO
T4
首先令\(f_{j,i}(j<i)\)表示以\(j、i\)结尾的斐波那契数列最长的长度,则其中任意初始值为\(2\),显然有如下状态转移方程:
类似于基础求子序列,很容易就能想到三重循环的\(O(n^3)\)做法,但是数据范围为\(n \le 3000\),最多要运算\(3e9\)次,肯定会超时,此时我们应该考虑如何优化。显然,由方程可得\((C_k=C_i-C_j)\),这样我们就可以直接计算得出\(C_k\),但仍需找到结果的位置,明显用哈希表就可以实现。
\(AC~Code:\)
#include <bits/stdc++.h>
using namespace std;
const int MAXN=3005;
int n,c[MAXN],f[MAXN][MAXN];
int main(){
freopen("fibonacci.in","r",stdin);
freopen("fibonacci.out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>c[i];
}
int ans=2;
f[1][2]=2;
for(int i=3;i<=n;i++){
for(int j=1;j<i;j++) f[j][i]=2;
unordered_map<int,int> mp(i*2);
for(int j=1;j<i;j++){
auto kp=mp.find(c[i]-c[j]);
if(kp!=mp.end()){
int k=kp->second;
f[j][i]=f[k][j]+1;
ans=max(ans,f[j][i]);
}
mp[c[j]]=j;
}
}
cout<<ans<<endl;
return 0;
}

浙公网安备 33010602011771号