两道很好的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;
}