2022/8/18 动态规划复习(内含纪念品,Caesar's Legions,数字游戏,合唱队形,The Battle of Chibi,Queries for Number of Palindromes)
前文
明天考试,然后放假,
我的心像四月的小鸟~ 🥤
Queries for Number of Palindromes
标签:回文类区间dp
一道典型的区间dp。注意求的是个数而不是长度。初始化的时候注意一下,len=2时分两种情况。ch[i]=ch[i-1]
时,dp[i-1][i]=3
。否则dp[i-1][i]=2
;
状态转移方程也十分简单,运用到了 容斥原理 。dp[i][j]=dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1]。
想起来其实挺妙的,本来像废话的一句话因为是按len从小到大枚举状态而变得十分有意义。
如果还不懂转移方程的看这边:
Code
#include<bits/stdc++.h> using namespace std; int dp[5005][5005]; char str[5005]; bool ha[5005][5005]; int main() { int t; scanf("%s%d",str+1,&t); int n=strlen(str+1); for(int i=1;i<=n;i++) { ha[i][i]=true; dp[i][i]=1; if(i>1) { if(str[i]==str[i-1]) ha[i-1][i]=true,dp[i-1][i]=3; else dp[i-1][i]=2; } } for(int len=3;len<=n;len++) { for(int l=1;l<=n-len+1;l++) { int r=l+len-1; dp[l][r]=dp[l][r-1]+dp[l+1][r]-dp[l+1][r-1]; if(ha[l+1][r-1]&&str[l]==str[r]) ha[l][r]=true,dp[l][r]++; } } int x,y; for(int i=1;i<=t;i++) { scanf("%d%d",&x,&y); printf("%d\n",dp[x][y]); } return 0; }
The Battle of Chibi
标签:线性dp,树状数组
分析:
设dp[i][j]以前j个数组成并以为a[j]为结尾的长度为i的严格递增子序列,
则状态转移方程:
i和j可以看出阶段,只会从小到大转移。k是DP的决策,两个限制条件:k < j 和a[k] < a[j] 。
可以先写出dp的朴素程序:
Code
// 例题:The Battle of Chibi // 暴力枚举决策 const int mod = 1000000007; memset(f, 0 ,sizeof(f)); a[0] = -(1<<30); // -INF f[0][0] = 1; for (int i = 1; i <= m; i++) for (int j = 1; j <= n; j++) for (int k = 0; k < j; k++) if (a[k] < a[j]) f[i][j] = (f[i][j] + f[i-1][k]) % mod; int ans = 0; for (int i = 1; i <= n; i++) ans = (ans + f[m][i]) % mod;
可是这O3 的时间复杂度很光荣的只得了15分。。。
考虑进行优化:
先对 dp 数组进行改造,假设 dp[ai][j] 代表:在数字序列的 [1,i] 区间内,以数字 a[i] 为结尾的长度为 j 的严格单增子序列的数目,这样一来,原来的 a[i] 的范围在 [1,1e9],数组是开不下的。所以,需要对 a[1∼n] 先进行离散化,使得所有 a[i] 的范围均在 [1,n],这样数组就开的下了。自然而然地,状态转移方程就变成了 dp[ai][j]=dp[1][j−1]+dp[2][j−1]+⋯+dp[ai−1][j−1]。
然后,既然要优化求前缀和的速度,不妨对 dp[1∼n][1] 构造一个树状数组,对 dp[1∼n][2] 构造一个树状数组,⋯,对 dp[1∼n][m] 构造一个树状数组。这样一来,我要求 dp[1][j−1]+dp[2][j−1]+⋯+dp[ai−1][j−1] 这样一个前缀和就可以在 O(logn) 时间内完成。总时间复杂度变为 O(n2logn)。那么最后所求答案即为 dp[1][m]+dp[2][m]+⋯+dp[n][m]。
Code
#include<bits/stdc++.h> #define lowbit(x) x&(-x) #define inf 0x3f3f3f3f using namespace std; typedef long long ll; const ll N = 1010; const ll mod = 1e9+7; ll n, k; ll a[N]; ll b[N]; ll dp[N][N]; void update(ll i, ll j, ll y) { y %= mod; while(i <= n) { dp[i][j] += y; dp[i][j] %= mod; i += lowbit(i); } } ll query(ll i, ll j) { ll ret = 0; while(i > 0) { ret += dp[i][j]; ret %= mod; i -= lowbit(i); } return ret; } int main() { ll t; scanf("%lld", &t); for(ll ca = 1; ca <= t; ca++) { memset(dp, 0, sizeof(dp)); scanf("%lld %lld", &n, &k); for(ll i = 1; i <= n; i++) { scanf("%lld", &a[i]); b[i] = a[i]; } sort(b+1, b+n+1); for(ll i = 1; i <= n; i++) { a[i] = lower_bound(b+1, b+1+n, a[i]) - b; } for(ll i = 1; i <= n; i++) { for(ll j = 1; j <= k; j++) { if(j == 1) update(a[i], 1, 1); else update(a[i], j, query(a[i]-1, j-1)); } } printf("Case #%lld: %lld\n", ca, query(n, k)); } return 0; }
合唱队形
although I know this problem is very water
I still want to write it。。。
正着求一遍最长上升子序列,再反着求一遍。然后枚举断点,也就是最高点。因为状态的意义是 以a[i] 结尾的最长上升子序列 。正着反着的结尾点都为a[i],显然正确。
很显然这个i会被算两次,减去即可
Code
#include <bits/stdc++.h> using namespace std; int arr[1005], dp[1005], dp1[1005], maxn = 1; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &arr[i]); } for (int i = 1; i <= n; i++) { dp[i] = 1; for (int j = 1; j <= i; j++) { if (arr[i] > arr[j]) { dp[i] = max(dp[i], dp[j] + 1); } } } for (int i = n; i >= 1; i--) { dp1[i] = 1; for (int j = i + 1; j <= n; j++) { if (arr[i] > arr[j]) { dp1[i] = max(dp1[i], dp1[j] + 1); } } } for(int i=1;i<=n;i++) { maxn=max(maxn,dp[i]+dp1[i]); } cout << n - maxn + 1; return 0; }
数字游戏
破环成链不必多说。设状态dp[i][j][k]的意思是在i到j中分k份的min/max.
显然,dp[i][j][k]=max(dp[i][j][k],dp[i][l][k-1]*(sum[j]-sum[l-1]))
核心代码
for(int k=2;k<=m;k++) { //长度len for(int i=1;i<=n*2;i++) { //左端点 for(int j=i+k-1;j<=n*2;j++) { //右端点 for(int l=i+k-2;l<=j-1;l++) { //中间点(总得在左右之间吧。。。) dp[i][j][k]=max(dp[i][j][k],dp[i][l][k-1]*(sum[j]-sum[l-1])); //min同理 } } } }
Caesar's Legions
设 dp[i][j][0/1] 表示放了i个步兵,j个骑兵的方案总数,其中排头为步兵或骑兵。0表示步兵排头,1表示骑兵排头。因为他们只能连续布置 k1和k2 个,因此,
可以得到状态转移方程式:
dp[i][j][0]=dp[i][j][0]+dp[i-k][j][1] (1<=k<=min(i,k1)) dp[i][j][1]=dp[i][j][1]+dp[i-k][j][0] (1<=k<=min(j,k2))
初始化代码如下:
for(int i=0;i<=k1;i++) dp[i][0][0]=1; for(int i=0;i<=k2;i++) dp[0][i][1]=1;
Code
#include<bits/stdc++.h> using namespace std; const int mod=1e8; int dp[105][105][2]; int main() { int n1,n2,k1,k2; scanf("%d %d %d %d",&n1,&n2,&k1,&k2); for(int i=0;i<=k1;i++) dp[i][0][0]=1; for(int i=0;i<=k2;i++) dp[0][i][1]=1; for(int i=1;i<=n1;i++) { for(int j=1;j<=n2;j++) { for(int k=1;k<=min(i,k1);k++) { dp[i][j][0]=(dp[i][j][0]+dp[i-k][j][1])%mod; } for(int k=1;k<=min(j,k2);k++) { dp[i][j][1]=(dp[i][j][1]+dp[i][j-k][0])%mod; } } } int ans=(dp[n1][n2][0]+dp[n1][n2][1])%mod; printf("%d",ans); return 0; }
纪念品
这题题面有一句关键的话,“当日购买的纪念品也可以当日卖出换回金币”!这句话可以帮我们简化状态,因为如果一个纪念品,你想连续持有若干天,可以看做第一天买,第二天早上立刻卖掉,然后第二天买回来,第三天早上立刻卖掉,然后第三天买回来……如果在未来的某一天里我们没有再选这一个物品,相当于就卖出去了。否则一直没卖。而第t - 1天买的东西,t 天都会卖掉。所以我们可以做t - 1次独立的完全背包,每一次先把所有的物品卖出去,然后再来买。
把今天手里的钱当做背包的容量,
把商品今天的价格当成它的消耗,
把商品明天的价格当做它的价值,
每一天结束后把总钱数加上今天赚的钱,直接写背包模板即可。
定义为用 j元钱去购买商品所能盈利的最大值
有状态转移方程:
dp[j]=max(dp[j], dp[j - price[k][i]] + price[k + 1][i] - price[k][i]);
Code
#include<bits/stdc++.h> using namespace std; int price[10005][10005],dp[10005]; int main() { int t,n,m; scanf("%d %d %d",&t,&n,&m); for(int i=1;i<=t;i++) { for(int j=1;j<=n;j++) { scanf("%d",&price[i][j]); } } for(int k=1;k<t;k++) { memset(dp,0,sizeof(dp)); for(int i=1;i<=n;i++) { for(int j=price[k][i];j<=m;j++) { dp[j]=max(dp[j],dp[j-price[k][i]]+price[k+1][i]-price[k][i]); } } m+=dp[m]; } printf("%d",m); return 0; }
本文来自博客园,作者:Doria_tt,转载请注明原文链接:https://www.cnblogs.com/pangtuan666/p/16597502.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统