2021JSCPC-队内选拔赛——两道DP题

两道很好的dp题:

oneplus的乘积

oneplus 有一个 n 长度的数列,以及一个数 x。
他想知道有多少种方式从这个数列中取出若干个数(至少一个),使得这些数的乘积为x。
答案可能很大,请输出对 1e9+7 取模后的值。

输入
第一行包含 2 个整数 n , x n,x n,x;(1 ≤ n, x ≤ 100000)
第二行包含 n 个整数,表示数列。

输出
输出一个整数表示答案

样例输入
10 180
1 2 2 2 3 3 3 5 5 6
样例输出
72

思路:

定义dp[i,j]:从前i个物品中选,乘积恰好为j的方案数。

朴素做法:
遍历所有的物品,遍历1~x,求出所有的dp[i,j],从而得出dp[n,m]
状态转移:
当前位置的方案数状态延续上一位置的方案数状态:dp[i,j] = dp[i-1,j];
如果当前数 ai 能被 j 整除的话,那么方案数加上乘积恰好为j/ai的方案数:dp[i-1,j/ai]

if(j%a[i]==0)  dp[i,j] += dp[i-1,j/a[i]];

但是物品个数一共1e5,乘积最大1e5,所以二重循环超时。

优化做法:

  • *时间优化: 因为最终答案状态只由 x 的所有因子转移过来,所以只需要更新所有因子的状态。 那么第二重循环就只需要遍历x的所有因子,个数很少。

  • #空间优化: 当前层状态只会用到上一层状态,所以用滚动数组优化为一维。 第二重循环从大到小遍历x的所有因子。

for(int i=1;i<=n;i++)
	for(int j=cnt;j>=1;j--)
		if(b[j]%a[i]==0)  dp[b[j]] += dp[b[j]/a[i]];

初始化:dp[1]=1,一开始乘积为1的方案数为1。

注:正是因为这个初始化,导致如果是求乘积为1的方案数的话,最终的答案多1个。 所以这种情况特判,方案数要-1。

Code:
const int N = 100010, mod = 1e9+7;
int T, n, m;
int a[N], b[N];
ll dp[N];

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	
	int cnt=0;
	for(int i=1;i<=m/i;i++)
	{
		if(m%i==0){
			b[++cnt]=i;
			if(m/i!=i) b[++cnt]=m/i;
		}
	}
	
	sort(b+1,b+cnt+1);
	
	dp[1]=1;
	for(int i=1;i<=n;i++)
	{
		for(int j=cnt;j>=1;j--)
		{
			if(b[j]%a[i]==0){
				dp[b[j]] += dp[b[j]/a[i]];
				dp[b[j]] %= mod;
			}
		}
	}
	
	if(m==1) dp[m]--;
	
	cout<<dp[m];
	
	/*朴素 
	dp[0][1]=1;
	
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=cnt;j++)
		{
			dp[i][b[j]]=dp[i-1][b[j]];
			
			if(b[j]%a[i]==0){
				dp[i][b[j]] += dp[i-1][b[j]/a[i]];
			}
		}
	}
	cout<<dp[n][m];*/ 
	
	return 0;
}

oneplus的数组2

给定一个长度为 n 的序列 a,以及一个整数 c。 (1 ≤ n, c ≤ 1e5, 1 ≤ a[i] ≤ 1e9)
一个长度为 k 的序列的值为序列中除了最小的 k/c 个元素(相同的元素有多少个算多少个)之外的所有元素之和。

例如 c = 2, [1, 2, 3, 4, 5] 的值为 3 + 4 + 5 = 12.

现在你需要将数组 a 划分成若干个连续的子序列,求所有划分方案中子序列的值之和的最小值。

输入
第一行两个整数n, c。
第二行n个整数,表示序列a。
输出
输出一行一个整数,表示所有划分方案中子序列之和的最小值。

样例输入
12 10
1 1 10 10 10 10 10 10 9 10 10 10
样例输出
92

思路:

由定义可知,将整个序列都划分为长度为1和长度为c的子序列最优。

那么题意就转化为:
可以将连续的 c 个位置合并为1个位置,合并后的值为 总和减去最小值。
问,最终所有位置之和的最小值。

下面就是要看如何合并。

定义 dp[i] 为,前 i 个位置合并后的的子序列值之和的最小值。

那么,

  • 如果当前的长度小于c,不能合并,dp[i]=sum[i];(sum[i] 前缀和)
  • 长度不少于 c,那么 dp[i] 就为:不合并当前位置 和 合并当前位置 取min。
    dp[i] = min(dp[i-1]+a[i],dp[i-c]+sum[i]-sum[i-c]-query(i-c+1,i)).

query(i,j):查询 [i,j] 这段区间中的最小值。可以用线段树、ST,因为区间长度一定,所以也可以用单调队列滑动窗口来实现。

Code:
const int N = 100010, mod = 1e9+7;
int T, n, m;
ll a[N], s[N];
ll f[N][40];
ll dp[N];

void init()
{
	for(int i=1;i<=n;i++) f[i][0]=a[i];
	
	int t=log(n)/log(2);
	for(int j=1;j<=t;j++){
		for(int i=1;i<=n-(1<<j)+1;i++){
			f[i][j]=min(f[i][j-1],f[i+(1<<(j-1))][j-1]);
		}
	}
}

int query(int l,int r)
{
	int t=log(r-l+1)/log(2);
	
	return min(f[l][t], f[r-(1<<t)+1][t]);
}

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i],s[i]=s[i-1]+a[i];
	
	init();
	
	for(int i=1;i<=n;i++)
	{
		if(i<m) dp[i]=s[i];
		else dp[i]=min(dp[i-1]+a[i],dp[i-m]+s[i]-s[i-m]-query(i-m+1,i));
	}
	cout<<dp[n];
	
	return 0;
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值