【算法每日一练]-动态规划 篇14 三倍经验 ,散步 ,异或和 ,抽奖概率

目录

 今日知识点:

金字塔的正反dp两种方案,转移方程取决于dp的具体含义

取模实现循环走m步回到原点的方案

计算上升子序列的异或和,只需在统计上升子序列时使用最小结尾元素进行标记

将亏本的概率转换各种情况的方案,然后统计亏本的情况的方案数烦求概率

三倍经验

散步

 异或和

抽奖概率 


        

        

三倍经验(两种解法)

思路:

首先不要考虑那么复杂,如果只是取数,但不考虑加倍的操作,那么就简单很多,只需要从下层想上层推导即可。保证每此都是最优解就行了。

这个时候f[i][j]从f[i-1][j]和f[i-1][j-1]中来。那么自然:

f[i][j]=max(f[i-1][j],f[i-1][j-1]) +a[i][j]。

然后我们再考虑要成3倍的情况,因为每个点可以对应是否有3倍的情况,而且这个消耗情况也要记录下来。所以需要开三维来表示。

解法一:(从上到下)

 设置f[i][j][k]表示在耗费次3倍操作下 且走到i,j对应的最优解。考虑到(i,j)可以由(i-1,j-1)和(i-1,j)过来。
 转移方程:

 f[i][j][l]=max(f[i-1][j][l],f[i-1][j-1][l])+a[i][j]; (当前数没有消耗次数)

 f[i][j][l]=max(f[i][j][l],max(f[i-1][j][l-1],f[i-1][j-1][l-1])+a[i][j]*3)(当前数消耗次数了)

转移顺序:因为每个(i,j)的l可能由很多个,所有需要把l放到最里面。
 

注意:小心数据中有负值,所以要初始化成无穷小,但是不要所有数据都变成无穷小了,我们把起始数据再处理一下就行了。最终需要在f[n][i][0~k]中找答案

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=105,INF=-3e9;
int n,k;
ll a[N][N],f[N][N][N],ans=INF;
int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++)//[0][?][?]的数据初始化为0,用于更新其他数据。而不是用INF去更新
	for(int j=0;j<=n;j++)
	for(int l=0;l<=k;l++)//需要存放结果的数据全部初始化成INF
	f[i][j][l]=INF;
	for(int i=1;i<=n;i++)
	for(int j=1;j<=i;j++){
		cin>>a[i][j];
		for(int l=0;l<=min(k,i);l++){
			if(l==0)
			f[i][j][l]=max(f[i-1][j][l],f[i-1][j-1][l])+a[i][j];//单独拿出来,防止越界
			else{
			f[i][j][l]=max(f[i-1][j][l],f[i-1][j-1][l])+a[i][j];
			f[i][j][l]=max(f[i][j][l],max(f[i-1][j][l-1],f[i-1][j-1][l-1])+a[i][j]*3);
				
			}
		}
	}
	for(int i=1;i<=n;i++)
	for(int l=0;l<=min(k,n);l++)
	ans=max(ans,f[n][i][l]);
	cout<<ans;
}

解法二:

当然也可以从下到上写:

 设置f[i][j][k]表示从i,j从为起点开始走到终点消耗k次对应的最优解。

那么(i,j)可以走到(i+1,j)和(i+1,j+1),那么(i,j)就由此决定。

转移方程:

                f[i][j][k]=max(f[i+1][j][k],f[i+1][j+1][k])+a[i][j];
                f[i][j][k]=max(f[i][j][k],max(f[i+1][j][k-1],f[i+1][j+1][k-1])+a[i][j]*3);  
 

同样注意一下初始化起始数据和其他数据。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll ans=-0x3f3f3f3f,f[110][110][110],a[110][110];
int n,m;
int main(){
	memset(f,-0x3f,sizeof(f));
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	for(int j=1;j<=i;j++)
		cin>>a[i][j];
	for(int i=1;i<=n;i++){
		f[n][i][0]=a[n][i];//这些是初始数据,用于更新其他数据的
		f[n][i][1]=a[n][i]*3;
	}
	for(int i=n-1;i>=1;i--)
	for(int j=1;j<=i;j++)
		for(int k=0;k<=min(n-i+1,m);k++){
			if(k==0)f[i][j][k]=max(f[i+1][j][k],f[i+1][j+1][k])+a[i][j];
			else{
				f[i][j][k]=max(f[i+1][j][k],f[i+1][j+1][k])+a[i][j];
				f[i][j][k]=max(f[i][j][k],max(f[i+1][j][k-1],f[i+1][j+1][k-1])+a[i][j]*3);	
			}
		}
			
	for(int i=0;i<=min(n,m);i++){
		ans=max(f[1][1][i],ans);
	}
	cout<<ans;
}

当然解法二还有另一种写法:(也是基于图论中的dp思想。)

就是在topo关系不明显的情况下,把一些点的影响扩散出去,更新周围点。

举个例子:在dijikstra和spfa的最短路算法中,本应该是dp[i]=min(dp[周围点j]+Wij),但是实际写法是:dp[周围点j]=min(dp[周围点j],dp[i]+Wij),相当于把i点的影响扩散出去来慢慢dp更新所有点的。

设置f[i][j][k]表示从i,j从开始消耗k次对应的最优解。

那么f[i-1][j-1]和f[i-1][j]就应该借此更新(对f[i-1][j-1]和f[i-1][j]产生影响):

(然后再拆成是否乘3倍,那就是4个式子)

            f[i-1][j-1][k]=max(f[i-1][j-1][k],f[i][j][k]+a[i-1][j-1]);
            f[i-1][j-1][k+1]=max(f[i-1][j-1][k+1],f[i][j][k]+a[i-1][j-1]*3);
            f[i-1][j][k]=max(f[i-1][j][k],f[i][j][k]+a[i-1][j]);
            f[i-1][j][k+1]=max(f[i-1][j][k+1],f[i][j][k]+a[i-1][j]*3); 
   

(可以结合下图理解)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll ans=-0x3f3f3f3f,f[110][110][110],a[110][110];
int n,m;
int main(){
	memset(f,-0x3f3f3f3f,sizeof(f));
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	for(int j=1;j<=i;j++)
		cin>>a[i][j];
	for(int i=1;i<=n;i++){
		f[n][i][0]=a[n][i];
		f[n][i][1]=a[n][i]*3;
	}
	for(int i=n;i>=2;i--)
	for(int j=1;j<=i;j++)
		for(int k=0;k<=min(n-i+2,m);k++){
			f[i-1][j-1][k]=max(f[i-1][j-1][k],f[i][j][k]+a[i-1][j-1]);
			f[i-1][j-1][k+1]=max(f[i-1][j-1][k+1],f[i][j][k]+a[i-1][j-1]*3);
			f[i-1][j][k]=max(f[i-1][j][k],f[i][j][k]+a[i-1][j]);
			f[i-1][j][k+1]=max(f[i-1][j][k+1],f[i][j][k]+a[i-1][j]*3);	
		}
	for(int i=0;i<=min(n,m);i++){
		ans=max(f[1][1][i],ans);
	}
	cout<<ans;
}

        

        

        

散步

思路:

 设置dp[i][j]表示已经走了i步,然后到达j有多少种走法。然后循环可以用取模实现,但是取模一定是0~n-1,所以需要进行映射。

转移方程:dp[i][j]=dp[i-1][(j+1)%n]+dp[i-1][(j-1+n)%n](从左右两个方向过来)

最终dp[m][0]就是答案。

初始化:注意把dp[0][0]初始化为1,为什么呢?因为要把<0,0>的影响扩散出去,也就是哪些点可以借助<0,0>进行更新,每次都是全图更新,从步数少到步数大,就可以更新出答案。

#include <bits/stdc++.h>
using namespace std;
int dp[35][35],n,m;
int main(){
	cin>>n>>m;
	dp[0][0]=1;
	for(int i=1;i<=m;i++)
	for(int j=0;j<n;j++)
		dp[i][j]=dp[i-1][(j+1)%n]+dp[i-1][(j-1+n)%n];
	cout<<dp[m][0];
}

        

         

 异或和

给一个长n的序列a1,a2……an,寻找在a的所有递增子序列(可以为空)的异或和中出现的数。

输入:                    输出:

2                                 4

1 5                              0 1 4 5

思路:

题意就是统计异或和,不过是仅统计所有上升子序列的异或和。不同于以往的上升子序列问题,以往的上升子序列问的是最长长度,所以我们常常设置f[i]表示i结尾对应的最长的长度。

而这次,我们无非是在更新上升子序列时候,不是去记录长度,而是记录对应的异或和,以便后面借此更新的上升子序列更新自己的异或和。

我们设置dp[i]表示以i结尾的上升子序列对应的异或和会如何呢?我们需要把每次更新的异或和都保存一下,最后输出。

        
也可以这么做:我们设置dp[i]表示异或和为i的上升子序列的最小结尾元素。

(其实是要满足所有同异或和的上升序列的更新情况,我们需要使用共异或和的上升子序列中的最小结尾元素打标记,而不是最大元素
dp[j]<a[i]时候(i可以拼在j后面):更新dp[j^a[i]]=min(dp[j^a[i],a[i])(标记了那个新异或和出现了)
最后统计有哪些dp被使用过,就说明这些数是答案

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,a[N],dp[N];

int main(){
	memset(dp,0x3f3f3f3f,sizeof(dp));
	dp[0]=0;
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++)//依次加入每个数
	for(int j=0;j<=550;j++)//枚举所有的异或和
		if(dp[j]<a[i]) //如果可以更新序列就更新异或和
		dp[j^a[i]]=min(dp[j^a[i]],a[i]);//看看以异或和为j^a[i]结尾的最小的元素变了没有
	vector<int> ans;
	for(int i=0;i<=550;i++)
		if(dp[i]!=0x3f3f3f3f) ans.push_back(i);
	cout<<ans.size()<<'\n';
	for(int i:ans)
		cout<<i<<" ";
	
}

        

        

抽奖概率 

有一个抽奖活动:抽一个2元,可能会抽出1,2,3,4元(概率相等)。

问抽n次,亏本的概率是多少(奖金小于本金)?纯赚超过一半本金的概率是多少

(分数时候输出最简分数)

输入:2           输出:3/16

                                   3/16

思路:

直接求概率不太容易。而且还要最简分数,那么就转化乘求方案数就会具体很多。

设置dp[i][j]表示已经抽奖i次且拿到了总额为j的方案数.dp[i][j]=dp[i-1][j-1,j-2,j-3,j-4]即可。

最后的最简分数可以使用gcd函数完成。
 

#include <bits/stdc++.h>
using namespace std;
int dp[40][160],n;
int gcd(int a,int b){return b==0?a:gcd(b,a%b);}

int main(){
	cin>>n;
	dp[1][1]=dp[1][2]=dp[1][3]=dp[1][4]=1;
	for(int i=1;i<=n;i++)
	for(int j=i;j<=4*n;j++){
		for(int k=1;k<=4;k++)
			if(j>k) dp[i][j]+=dp[i-1][j-k];
	}
	int sum1=0,sum2=0,sum=1;
	for(int i=n;i<2*n;i++)
		sum1+=dp[n][i];
	for(int i=3*n+1;i<=4*n;i++)
		sum2+=dp[n][i];
	for(int i=1;i<=n;i++)
		sum*=4;
	int k=gcd(sum1,sum);
	cout<<sum1/k<<"/"<<sum/k<<'\n';
	k=gcd(sum2,sum);
	cout<<sum2/k<<"/"<<sum/k<<'\n';

}

  • 39
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值