Codeforces Round #523 (Div. 2) C. Multiplicity 【dp】

本文详细解析了CodeForces平台上的C题解题思路,采用动态规划方法,通过求解数列中各元素的约数来更新DP状态,实现高效求解。文章强调了约数顺序的重要性及时间复杂度的优化。

题目链接:http://codeforces.com/contest/1061/problem/C

思路:

像类似这种求有衔接关系的数列要第一时间想到  dp[ i ] = dp[ i ] + dp[ i - 1 ] 这种状态的dp,为什么?

假设我要让一个原数组下标为 4的数变成下标为 3 的数,那么要想数列存在,那么 必须存在 至少一个 长度为2的数列,在这些数列的尾部加上这个原数组下标为4的数才能构成一个长度为 3 的数列,由此对应的长度为3的个数就是长度为2的个数,加上本身存在的长度为 3的个数,所以得到  dp[ i ] = dp[ i ] + dp[ i - 1] ;

如何确定一个数放在下标 小于原下标的位置(因为数列只能通过删除某一个数才能改变原数组下标,所以数组下标只可能小不可能大于自身)

只要求出当前这个数的所有约数,对应的约数就是能放置的位置,通过 n*sqrt (n) 时间复杂度的算法能够求出所有数的个数。

这里有几点需要注意的:

1,约数放置到vector里面的时候,一定要按顺序放置,通过stack的辅助代替sort的使用,减少时间复杂度,为什么要按顺序放置,当你在遍历约数的时候,必须要从大到小遍历,如果你按小到大,或者随便遍历,当你更新约数大的dp,有可能会使用到你刚刚才更新过的约数小的dp,这时候结果就是错的了(自己纸上模拟一下就知道了,跟背包空间优化的逆序遍历dp是一样的道理,如果你用二维dp自然就不用考虑这个)

2,如果当前遍历到的约数比原数组的最大下标还要大时直接不考虑。

之前一直觉得用n*sqrt(n) 的算法求约数会超时,写到后面才发现,遍历dp的时候,我们并不需要从头到尾逐个更新dp的值,因为对应的约数就是dp的位置了,所以直接遍历约数就行了,一个数的约数一般不会太大,所以时间是很充裕的。

#include <bits/stdc++.h>

using namespace std;

const int Maxn = 1e5+10;
const int Mod = 1e9+7;

int b[Maxn], dp[Maxn];
vector <int> a[Maxn];
stack <int> sk;

void solve (int x) { // 约数打表
    int tmp = b[x];
    for (int i = 1; i*i <= tmp; ++i) {
        if (tmp % i != 0) continue;
        if (i*i == tmp) {
            a[x].push_back (i);
        } else {
            a[x].push_back (i); sk.push(tmp/i);
        }
    }
    while (!sk.empty()) {  // 保证数列是从小到大排列的,为dp的逆序
        a[x].push_back (sk.top());
        sk.pop();
    }
}

int main (void)
{
    int n, tmp;
    scanf ("%d", &n);
    for (int i = 0; i < n; ++i) {
        scanf ("%d", &b[i]);
        solve (i);
    }
    dp[0] = 1;
    for (int i = 0; i < n; ++i) {
        int m = a[i].size();
        for (int j = m-1; j >= 0; --j) {  // 从大到小遍历更新dp
            int tmp = a[i][j];
            if (tmp <= n && dp[tmp-1]) { // 保证约数在n范围内,超出n的约数不考虑
                dp[tmp] = (dp[tmp-1]+dp[tmp]) % Mod;
            }
        }
    }
    int ans = 0;
    for (int i = 1; i <= n; ++i) {
        ans = (ans + dp[i]) % Mod;
    }
    printf ("%d\n", ans);
    return 0;
}

 

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值