DP刷题记录
美丽序列
https://ac.nowcoder.com/acm/problem/21313?&headNav=acm
题目描述
牛牛喜欢整数序列,他认为一个序列美丽的定义是
1:每个数都在0到40之间
2:每个数都小于等于之前的数的平均值
具体地说:for each i, 1 <= i < N, A[i] <= (A[0] + A[1] + ... + A[i-1]) / i.
3:没有三个连续的递减的数
现在给你一个序列,每个元素是-1到40,你可以将序列中的-1修改成任意的数,求你可以得到多少个美丽序列,答案对1e9+7取模
输入描述:
第一行输入一个整数n (1 ≤ n ≤ 40)
第二行输入n个整数
输出描述:
输出一个整数
示例1
输入
2
3 -1
输出
4
示例2
输入
3
5 3 -1
输出
2
示例3
输入
3
-1 0 40
输出
0
示例4
输入
11
-1 40 -1 -1 -1 10 -1 -1 -1 21 -1
输出
579347890
思路
这个题主要是状态的表示:
f[i][j][k][w]:第i位数字为j,第j位数字为k,和为w的方案数。
记录这几个状态就可以转移了。但是你发现初始化麻烦和复杂度很高O(n^6)
如果我们把第3维k的含义表示为:递减的长度
初始化方便,而且复杂度O(n^5)
#include<bits/stdc++.h>
#define LL long long
using namespace std;
LL f[41][41][4][1601];
int a[45];
const int mod=1e9+7;
int main() {
int n;
scanf("%d", &n);
for(int i=1; i<=n; i++) {
scanf("%d", &a[i]);
}
f[0][0][1][0]=1;
for(int i=0; i<n; i++) {
for(int j=0; j<=40; j++) {
for(int k=1; k<=2; k++) {
for(int w=0; w<=i*40; w++) {
if(f[i][j][k][w]) {
if(a[i+1]==-1) {
for(int z=0; z<=40; z++) {
int pos=i?(w/i):40;
if(z<=pos) {
if(z<j) {
f[i+1][z][k+1][w+z]+=f[i][j][k][w];
f[i+1][z][k+1][w+z]%=mod;
} else {
f[i+1][z][1][w+z]+=f[i][j][k][w];
f[i+1][z][1][w+z]%=mod;
}
}
}
} else {
int pos=i?(w/i):40;
if(a[i+1]<=pos) {
if(a[i+1]<j) {
f[i+1][a[i+1]][k+1][w+a[i+1]]+=f[i][j][k][w];
f[i+1][a[i+1]][k+1][w+a[i+1]]%=mod;
} else {
f[i+1][a[i+1]][1][w+a[i+1]]+=f[i][j][k][w];
f[i+1][a[i+1]][1][w+a[i+1]]%=mod;
}
}
}
}
}
}
}
}
int ans=0;
for(int i=0; i<=40; i++) {
for(int j=1; j<=2; j++) {
for(int k=0; k<=40*n; k++) {
ans+=f[n][i][j][k];
ans%=mod;
}
}
}
printf("%d\n", ans);
return 0;
}
排序背包
题目描述
牛牛正在打一场CF
比赛时间为T分钟,有N道题,可以在比赛时间内的任意时间提交代码
第i道题的分数为maxPoints[i],题目的分数随着比赛的进行,每分钟减少pointsPerMinute[i]
这是一场比较dark的Cf,分数可能减成负数
已知第i道题需要花费 requiredTime[i] 的时间解决
请问最多可以得到多少分
输入描述:
第一行输入两个整数N,T (1 ≤ N ≤ 50, 1 ≤ T ≤ 100000)
第二行输入n个整数maxPoints[i]
第三行输入n个整数pointsPerMinute[i]
第四行输入n个整数requiredTime[i]
1 ≤ maxPoints[i],pointsPerMinute[i],requiredTime[i] ≤ 100000
输出描述:
输出一个整数
示例1
输入
1 74
502
2
47
输出
408
示例2
输入
复制
2 40000
100000 100000
1 100000
50000 30000
输出
0
示例3
输入
3 75
250 500 1000
2 4 8
25 25 25
输出
1200
示例4
输入
3 30
100 100 100000
1 1 100
15 15 30
输出
97000
备注:
子任务1: n <= 10
子任务2: n <= 20
子任务3: 无限制
思路:
假设最佳方案有x题和y题
如果x题先写:
s1=
如果y题先写:
s2=
假设x题在前面你们就要满足:
s1<=s2化简后对题目排序就可以了。然后跑背包
#include<bits/stdc++.h>
#define LL long long
using namespace std;
struct Node{
LL w, a, t;
}a[55];
LL f[100005];
int main() {
int n, T; scanf("%d%d", &n, &T);
for(int i=1; i<=n; i++){
scanf("%lld", &a[i].w);
}
for(int i=1; i<=n; i++){
scanf("%lld", &a[i].a);
}
for(int i=1; i<=n; i++){
scanf("%lld", &a[i].t);
}
sort(a+1, a+1+n, [](Node &a, Node &b){return a.t*b.a<b.t*a.a;});
for(int i=1; i<=n; i++){
for(int k=T; k>=a[i].t; k--){
f[k]=max(f[k], f[k-a[i].t]-k*a[i].a+a[i].w);
}
}
printf("%lld\n", *max_element(f, f+T+1));
return 0;
}
贪心+概率DP(网格游戏 开格子)
https://ac.nowcoder.com/acm/problem/21797
题目描述
有一个游戏平板上面有n×m个格子,一开始每个格子都是关闭的,每个格子里面都有一个标记
已知每种标记恰好出现两次,也就是一共有n*m/2种标记
规定一次移动为依次(one by one不是同时)打开一对格子查看里面的标记,如果标记不一样,格子会自动关闭,但是你的记忆是超强了,看过了就不会忘,如果标记是一样的,格子从此就一直保持打开状态,当所有格子都打开时游戏结束
请算出游戏结束的最少期望步数
输入描述:
输入一行包含两个整数 N, M (1 ≤ N ≤ 50, 1 ≤ M ≤ 50)
N*M是偶数
输出描述:
输出一个浮点数.误差不超过1e-9
示例1
输入
1 2
输出
1.0
示例2
输入
2 2
输出
2.6666666666666665
示例3
输入
2 3
输出
4.333333333333334
示例4
输入
4 4
输出
12.392984792984793
备注:
子任务1: n * m <= 100
子任务2: n * m <= 1000
子任务3: 无限制
思路
我们能够很快想到思路:
DP(x, y):已经打开x个,有y个没有匹配的期望步数
那么答案就是DP(0, 0)
考虑转移:因为一次可以打开2个格子,并且格子不是同时打开了,那么我们可以打开一个再选择最佳方案。
当前状态我们肯定要打开格子:
- 如果打开的第一个格子是之前没有匹配的P=y/(N-a)
那么下次肯定是打开之前没有匹配的格子。
- 如果打开的第一个格子是之前没有出现过的(1-P)
那么打开第二个格子有3种情况
1.和刚打开的格子匹配
2.和之前没有匹配的格子匹配,那么下次肯定是打开这两个格子
3.和所有的格子不匹配
最后f[x][y]+=1,因为这次打开了格子。
记忆化DP就可以了
#pragma comment(linker, "/STACK:102400000,102400000")
#include <bits/stdc++.h>
#define LL long long
using namespace std;
int N;
double f[2501][1251];
bool vis[2501][1251];
double DP(int a, int b){
if(b<0) return 0;
if(a>=N) return 0;
if(b>N-a) return 0;
if(vis[a][b]) return f[a][b];
vis[a][b]=1;
LL x=N-a;
double p=b*1.0/(x);
double ans=DP(a+1, b-1)*p;
if(x>1){
double p1=1.0*(1)/(x-1);
double p2=1.0*(b)/(x-1);
double p3=(1.0-p1-p2);
ans+=(1-p)*p1*DP(a+2, b)+(1-p)*p2*(DP(a+2, b)+1)+(1-p)*p3*DP(a+2, b+2);
}
return f[a][b]=ans+1;
}
int main(){
int n, m; scanf("%d%d", &n, &m);
N=n*m;
printf("%.11f\n", DP(0, 0));
return 0;
}
DP+博弈(棋盘的必胜策略)
题目描述
有一个二维棋盘,棋盘有r行c列,棋盘中的每一个位置有如下四种情况
'E': 表示出口,可能有多个
'T': 只有一个,表示起点
'#': 表示障碍
'.': 表示空地
牛牛和牛妹在这样一个棋盘上玩游戏,他们有一张写有整数k的卡片,一开始放置在起点的位置,现在牛牛和牛妹开始轮流操作,牛牛先操作
当前操作的牛会选择上下左右其中一个方向移动卡片,每走一步,卡片上的数字减去1
只能走到空地上, 或者走到出口,走到出口,游戏就会结束,卡片的数字变成0的时候游戏也会结束,不能再移动的牛会输掉游戏
如果牛牛和牛妹都用最佳策略,请问谁会赢
输入描述
第一行输入3个整数r,c,k
接下来r行每行读入k个字符表示棋盘
1 ≤ r,c ≤ 50, 1 ≤ k ≤ 100
输出描述
如果牛牛有必胜策略,输出"niuniu"
否则输出"niumei"
示例1
输入
2 3 3
T.#
#.E
输出
niuniu
示例2
输入
3 3 99
E#E
#T#
E#E
输出
niumei
备注:
子任务1:mac(r,c) <= 10
子任务2:max(r,c) <= 20
子任务3:无限制
思路:
f[x][y][k]:在(i, j)数字为k的先手的胜负态.
结果sg定理转移就可以了。
#include<bits/stdc++.h>
using namespace std;
char s[55][55];
int f[55][55][105];
int xx[4]={1, -1, 0, 0};
int yy[4]={0, 0, 1, -1};
int r, c;
int sg(int i, int j, int k){
if(s[i][j]=='E'||k==0) return 0;
if(f[i][j][k]!=-1) return f[i][j][k];
f[i][j][k]=0;
for(int kk=0; kk<4; kk++){
int x=i+xx[kk], y=j+yy[kk];
if(x>=1&&x<=r&&y>=1&&y<=c&&s[x][y]!='#'){
if(!sg(x, y, k-1)) f[i][j][k]=1;
}
}
return f[i][j][k];
}
int main(){
memset(f, -1, sizeof(f));
int k; scanf("%d%d%d", &r, &c, &k);
for(int i=1; i<=r; i++){
scanf("%s", s[i]+1);
}
int x=0, y=0;
for(int i=1; i<=r; i++){
for(int j=1; j<=c; j++){
if(s[i][j]=='T'){
x=i, y=j;
}
}
}
if(sg(x, y, k)){
printf("niuniu\n");
}
else{
printf("niumei\n");
}
return 0;
}
和与或(数位+状压)
https://ac.nowcoder.com/acm/problem/21336
题目描述
给你一个数组R,包含N个元素,求有多少满足条件的序列A使得
0 ≤ A[i] ≤ R[i]
A[0]+A[1]+...+A[N-1]=A[0] or A[1]... or A[N-1]
输出答案对1e9+9取模
输入描述:
第一行输入一个整数N (2 ≤ N ≤ 10)
第二行输入N个整数 R[i] (1 ≤ R[i] ≤ 1e18)
输出描述:
输出一个整数
示例1
输入
2
3 5
输出
15
示例2
输入
3
3 3 3
输出
16
示例4
输入
4
26 74 25 30
输出
8409
示例5
输入
2
1000000000 1000000000
输出
420352509
思路
根据条件我们可以推出来:每个A[i]在二进制下占的位不能重复。
n<=1e18,在二进制下就60位,我们考虑数位DP。
DP(pos, bool x1, bool x2, ... bool xn)
考虑到第pos位,每个个A[i]是否有限制。
但是n=10,所以x数组用int状压一下。
转移时,第i位考虑为0,或者为1。为1时把这位1分配到哪个A[i]上。是否限制,考虑完全就可以了。
#include <bits/stdc++.h>
#define LL long long
using namespace std;
LL a[100005];
LL f[70][1025];
const int mod=1e9+9;
int n;
int dfs(int pos, int ok){
if(pos==-1) return 1;
LL &ans=f[pos][ok];
if(ans>=0) return ans;
int is=0;
for(int i=0; i<n; i++){
//如果pos为0,所有的A[i]的限制变化
if(ok&(1<<i)&&!(a[i]&(1ll<<pos))) is^=1<<i;
}
ans=dfs(pos-1, is);//pos为0时
//枚举把pos位1分配给谁
for(int i=0; i<n; i++){
if(!(ok&1<<i)||a[i]&(1ll<<pos)){
ans+=dfs(pos-1, (ok&1<<i)&&a[i]&(1ll<<pos)?(is^1<<i):is);
}
ans%=mod;
}
return ans;
}
int main(){
scanf("%d", &n);
for(int i=0; i<n; i++){
scanf("%lld", &a[i]);
}
memset(f, -1, sizeof(f));
printf("%d\n", dfs(60, (1<<n)-1));
return 0;
}
牛牛的计算机内存(状压DP)
https://ac.nowcoder.com/acm/problem/21873
题目描述
牛牛的计算机一共有m块内存,现在有n条指令,每条指令是一个01序列
如果指令的第i个字符为1,说明这条指令需要访问第i块内存
每条指令的执行代价为k^2, k为新访问内存的数量,即之前的指令都没有访问到的内存数量
你可以随意安排n条指令的执行顺序,求执行完所有指令的最小代价
输入描述:
第一行先输入一个整数n (1≤n≤20)
接下来每行输入一个01字符串,长度不超过20
输出描述:
输出一个整数
示例1
输入
3 3
111
001
010
输出
3
示例2
输入
5 5
11101
00111
10101
00000
11000
输出
9
示例3
输入
3 4
1000
1100
1110
输出
3
思路
n<=20,因为要考虑顺序,我们自然想到状压dp,f[s]:把s集合的指令执行完的最小代价。
转移时我们要知道s集合占用了哪些内容,我们另外用一个数组ok[s]:二进制状压s集合的指令执行完后,
的内容访问状态。
ok[s]:可以转移时确定,也可以预处理。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
char a[25][25];
int num[25];
int ok[1<<22];
int f[1<<22];
int n, m;
void getok(int i, int s1, int s2){
if(i==n){
ok[s1]=s2;
return ;
}
getok(i+1, s1|(1<<i), s2|num[i]);
getok(i+1, s1, s2);
}
int sum(int s1, int s2){
int res=0;
for(int i=0; i<m; i++){
if((s1&(1<<i))&&!(s2&(1<<i))){
res++;
}
}
return res*res;
}
int main(){
scanf("%d%d", &n, &m);
for(int i=0; i<n; i++){
scanf("%s", a[i]+1);
int s=0;
for(int k=1; k<=m; k++){
s=s*2+a[i][k]-'0';
}
num[i]=s;
}
getok(0, 0, 0);
memset(f, 0x3f, sizeof(f));
f[0]=0;
for(int s=1; s<(1<<n); s++){
for(int i=0; i<n; i++){
if(s&(1<<i)){
f[s]=min(f[s], f[s^(1<<i)]+sum(num[i], ok[s^(1<<i)]));
}
}
}
printf("%d\n", f[(1<<n)-1]);
return 0;
}
牛牛去买球
https://ac.nowcoder.com/acm/problem/21668
题目描述
一个人如果在他很小的时候就自己赚过钱,他的一生都会过得非常节俭,因为他知道财富来之不易.
作为一个勤俭节约的好孩子,牛牛决定在生活中践行这一原则.
有一天他想去商店买一些球来玩,他发现商店里有n个盒子,每个盒子外面有一张标签告诉你有ai个红球,bi个蓝球,需要ci的钱购买
但是由于店主是一个粗心的人,他告诉你每个盒子球的总量是符合标签的说明的,但是具体的种类可能会有如下偏差,比如可能有
(ai+1 ,bi-1),(ai, bi), (ai-1, bi+1)三种可能
牛牛 想要买至少K个同颜色的球,但是他又不想浪费钱.
帮他算算最少花多少钱买盒子能够使得至少会有K个球是同色的
输入描述:
第一行输入两个整数n,K (1≤ n≤50, 1 ≤ K ≤ 10000)
第二行输入n个整数表示a数组
第三行输入n个整数表示b数组
第三行输入n个整数表示c 数组
1 ≤ ai,bi,ci ≤ 10000
输出描述:
输出一个整数,如果无法达成目的,输出
示例1
输入
2 10
6 5
4 4
1 1
输出
2
示例2
输入
2 10
5 5
4 4
1 1
输出
-1
示例3
输入
1 9
10
5
13
输出
13
示例4
输入
5 10
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
输出
10
示例5
输入
5 17
1 2 3 4 15
1 2 3 4 5
1 2 3 4 5
输出
9
思路
我们考虑2个极端的情况:
1.我们选择a选到k,并且每个盒子都是A[i]-1
2.我们选择b选到k,并且每个盒子都是B[i]-1
当然这个思路一些问题。因为都是A[i]-1的时候,可能选择b更快。
我们考虑什么时候,一定有k个一种类型的球。
min(max(x, y))=k。那么x+y=2k-1。
所以我们选择球的总数>=2k-1时,一定可行。
所以跑3个背包就可以了。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
int a[55], b[55], c[55];
int f[20005];
int main(){
int n, k; scanf("%d%d", &n, &k);
for(int i=1; i<=n; i++) scanf("%d", &a[i]);
for(int i=1; i<=n; i++) scanf("%d", &b[i]);
for(int i=1; i<=n; i++) scanf("%d", &c[i]);
memset(f, 0x3f, sizeof(f));f[0]=0;
int ans=f[1];
for(int i=1; i<=n; i++){
for(int j=20000; j>=a[i]-1; j--){
f[j]=min(f[j], f[j-(a[i]-1)]+c[i]);
}
}
for(int i=k; i<=20000; i++) ans=min(ans, f[i]);
memset(f, 0x3f, sizeof(f));f[0]=0;
for(int i=1; i<=n; i++){
for(int j=20000; j>=b[i]-1; j--){
f[j]=min(f[j], f[j-(b[i]-1)]+c[i]);
}
}
for(int i=k; i<=20000; i++) ans=min(ans, f[i]);
memset(f, 0x3f, sizeof(f));f[0]=0;
for(int i=1; i<=n; i++){
for(int j=20000; j>=a[i]+b[i]; j--){
f[j]=min(f[j], f[j-(a[i]+b[i])]+c[i]);
}
}
for(int i=2*k-1; i<=20000; i++){
ans=min(ans, f[i]);
}
if(ans==f[20001]){
printf("-1\n");
}
else{
printf("%d\n", ans);
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)