Music Problem——01背包优化

题目链接:https://ac.nowcoder.com/acm/contest/5203/B

一道很不错的题目,综合了多个知识点。

题意:给你n个数,判断能否在这n个数中选一些数组成3600的倍数。

先给这个题的一般形式。

给你n个数,判断能否在这n个数中选一些数组成m的倍数。

(1 ≤ n ≤ 1e6, 2 ≤ m ≤ 1e3)

 然后我们可以发现一个01背包的解法(时间复杂度:O(n*m))

状态:dp[i][j]表示从1到i个数中能否选一些数组成m(0表示不可以,1表示可以)。

状态转移方程: dp[i + 1][(j + a[i]) % m] = 1 if dp[i][j] = 1

这里可以考虑用滚动数组优化一下。

但是这个时间复杂度肯定过不去,然后我们考虑优化。

有三种优化策略:二进制优化,bitset 或者 抽屉原理。

这里主要讲抽屉原理优化,其余的优化也会给出代码。

当n>=m的时候,肯定可以从这n个数中选择一些数组成m的倍数,直接输出YES即可。

为什么呢?利用一个取余的性质( (a + b) % mod = (a%mod +b%mod) % mod )以及抽屉原理.

下面给出证明(借鉴牛客上大佬的解释,链接:https://ac.nowcoder.com/acm/problem/blogs/13885):

定义sum数组为这n个数的前缀和数组,即sum[i]=a[1]+a[2]+.....a[i-1]+a[i]

分为两种情况考虑:

情况一:存在一个k(1 <= k <= n),使得sum[k] % m == 0,那么就得证;
情况二:对于任意的k(1 <= k <= n),都有sum[k] % m != 0,那么余数的种类只有 1 到 m-1 总共 m-1 种情况,但是有 m 个 sum,由抽屉原理可知必然有两个sum(假设为sum[i],sum[j],j > i)的余数相同。因此 (sum[j] - sum[i]) % m = (sum[j] % m - sum[i] % m) = 0;

代码实现:

#include<stdio.h>
#include<string.h>
using namespace std;
int a[150000];
int dp[3605],f[3605];
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        int n;scanf("%d",&n);
        int m=3600;
        memset(dp,0,sizeof(dp));
        memset(f,0,sizeof f);
        for(int i=1;i<=n;i++) scanf("%d",&a[i]);
        if(n>3600) printf("YES\n");
        else{
            for(int i=1;i<=n;i++){
                for(int j=0;j<=m-1;j++) f[j]=dp[j];
                for(int j=m-1;j>=1;j--)
                    if(dp[j])
                        f[(j+a[i]%m)%m]=1;
                f[a[i]%m]=1;
                for(int j=0;j<=m-1;j++) dp[j]=f[j];
            }
            if(dp[0]) printf("YES\n");
            else printf("NO\n");
        }
    }
}

二进制优化的代码(来自已退役的cds学长):

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int V, s[3600], dp[18005];
void Alone(int use)
{
    int v;
    for(v=V;v>=use;v--)
    {
        if(dp[v-use]==1)
            dp[v] = 1;
    }
}
void Every(int use)
{
    int v;
    for(v=use;v<=V;v++)
    {
        if(dp[v-use]==1)
            dp[v] = 1;
    }
}
void Mulit(int n, int use)
{
    int k;
    k = 1;
    while(k<n)
    {
        Alone(k*use%3600);
        n -= k;
        k *= 2;
    }
    Alone(n*use%3600);
}
int main(void)
{
    int T, n, i, x;
    V = 18000;
    scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        memset(s, 0, sizeof(s));
        for(i=1;i<=n;i++)
        {
            scanf("%d", &x);
            x %= 3600;
            s[x]++;
        }
        if(s[0])
        {
            printf("YES\n");
            continue;
        }
        memset(dp, 0, sizeof(dp));
        dp[0] = 1;
        for(i=1;i<=3599;i++)
        {
            if(s[i]!=0)
                Mulit(s[i], i);
        }
        for(i=1;i<=5;i++)
        {
            if(dp[3600*i])
            {
                printf("YES\n");
                break;
            }
        }
        if(i==6)
            printf("NO\n");
    }
    return 0;
}
/*
2
3
1800 3500 1900
6
3000 3000 3000 3000 3000 3000
*/

bitset优化:

#include <bits/stdc++.h>
using namespace std;
int T,n;
int a[100005];
int main(){
    scanf("%d",&T);
    while (T--)
    {
        scanf("%d",&n);
        for (register int i=1; i<=n; ++i) scanf("%d",&a[i]),a[i]%=3600;   
        bitset<7201>dp;
        for (register int i=1; i<=n; ++i)
        {
            dp|=(dp<<a[i])|(dp<<a[i]>>3600);
            dp[a[i]]=1;
        }
        if (dp[0]) puts("YES");
        else puts("NO");
    }
return 0;   
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值