Fork me on GitHub

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的严格递增子序列,

则状态转移方程:

dp[i][j]={\sum_{k=0}^{j}}dp[i-1][k] \left ( a[k]<a[j] \right )

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;
}

 

posted @   Doria_tt  阅读(23)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
点击右上角即可分享
微信分享提示