第十六届上海大学程序设计联赛 L-K序列

https://www.nowcoder.com/acm/contest/91/L

题目描述

给一个数组 a,长度为 n,若某个子序列中的和为 K 的倍数,那么这个序列被称为“K 序列”。现在要你 对数组 a 求出最长的子序列的长度,满足这个序列是 K 序列。 

输入描述:

第一行为两个整数 n, K, 以空格分隔,第二行为 n 个整数,表示 a[1] ∼ a[n],1 ≤ n ≤ 105 , 1 ≤ a[i] ≤ 109 , 1 ≤ nK ≤ 107

输出描述:

输出一个整数表示最长子序列的长度 m

示例1

输入

7 5

10 3 4 2 2 9 8

输出

6

时间限制:1s

空间限制:131072K

题目的题意很明确,从n个数里选m个,使m个数的和是k的倍数,求最大的m

扫描二维码关注公众号,回复: 11354837 查看本文章

朴素算法:选1个,选2个,选3个……选n个都试一下。复杂度过大,暴力是不可能AC的

改良算法:选n个,如果选n个可以的话直接break输出n,如果不可以试n-1,n-2……

假如出题人找到某组测试样例n很大,而答案m很小,这种算法就很有可能TLE,而敲出来这种风险代码耗费的时间显然不值得我去尝试。

所以要寻找更高效的算法

ZOJ 1569 对于给定序列,问有多少子序列的和是m倍数

找到这样一个博客,里面介绍了求子序列个数的方法:

对n个数求前缀和,然后对k取模

即,假设n个数为num[0],sum[1]……sum[n-1]

其前i项之和为sum[i-1]

然后对sum数组每一项对k取模得到新的sum数组

如果当sum[i]==sum[j]的时候,那么必然就有∑num(i+1->j)为k的倍数

由于sum对k取模之后取值[0,k-1],所以对sum的每个可能取值,找其在sum数组里出现的最前位置i和最后位置j

则∑num(i+1->j)%k == 0 (i,j存在,且i < j)

记[i+1,j]为一条线段

则最终要求的“最长子序列”问题转化为求k条[i+1,j]线段在[0,n-1]上覆盖的区间长度。

但是想法还有漏洞,如果第一项为10,k取5,那么第一项是0,本身%k为0而后续的sum数组里没有配对的0,那么似乎它就被可怜地遗漏了。

所以打了补丁,if(!sum[i])数轴上把这点做标记

至此似乎没有了问题,问题转化为求k条[i+1,j]线段在[0,n-1]上覆盖的整点个数。

//WA代码
#include <bits/stdc++.h>
using namespace std;
int num[100010];
int val[100010];
bool res[10000010];
int main(){
    int n,k;
    scanf("%d%d",&n,&k);
    for(int i = 1;i <= n;i++){
        scanf("%d",&num[i]);
        val[i] = val[i-1]+num[i];
        val[i]%=k;
    }
    for(int i = 1;i <= n;i++)
    if(!val[i])memset(res+1,1,i);
    for(int i = 0;i < k;i++){
        int fir = 1,sec = n;
        while(fir<=n && val[fir]!=i)fir++;
        while(sec>0 && val[sec]!=i)sec--;
        if(sec)
        memset(res+fir,1,sec-fir);
    }
    int ans = 0;
    for(int i = 1;i < 10000010;i++)
    if(res[i])ans++;
    printf("%d\n",ans);
    return 0;
}

提交上去居然WA了,难道说这种算法还有什么漏洞吗?

将代码块放到do...while语句里检错:

#include <bits/stdc++.h>
using namespace std;
int num[100010] = {0,10,3,4,2,2,9,8};
int val[100010];
bool res[10000010];
int main(){
    int n = 7,k = 5;
    //scanf("%d%d",&n,&k);
    do{
    	for(int i = 1;i <= n;i++){
	        //scanf("%d",&num[i]);
	        val[i] = val[i-1]+num[i];
	        val[i]%=k;
	    }
	    for(int i = 1;i <= n;i++)
	    if(!val[i])memset(res+1,1,i);
	    for(int i = 0;i < k;i++){
	        int fir = 1,sec = n;
	        while(fir<=n && val[fir]!=i)fir++;
	        while(sec>0 && val[sec]!=i)sec--;
	        if(sec)
	        memset(res+fir,1,sec-fir);
	    }
	    int ans = 0;
	    for(int i = 1;i < 10000010;i++)
	    if(res[i])ans++;
	    if(ans != 6){
	    	cout << "Wrong ans is: " << ans << endl;
	    	for(int i = 1;i <= n;i++)cout << num[i] << " ";
			cout << endl; 
		}
		memset(res,0,sizeof(res));
	} while(next_permutation(num+1,num+1+n));//换num数组里每个数的相对位置
    return 0;
}

错误数据组数还是很多的,找一个错得最离谱的进行分析,根据自己的算法,能否得出正确答案。如果能,说明程序写得有bug,如果没有,问题就大了,还要找新的算法。

num数组 10 4 3 8 9 2 2

前缀和 10 14 17 25 34 36 38

%k后 0 4 2 0 4 1 3

根据我的算法,两个0匹配,两个4匹配,而1和3没有匹配的数字而被抛弃- -手算答案是5,机算答案是4,正确答案是6

好嘛,算法也不对,代码也不对。

错误原因分析

代码错误,错在了memset对线段标记的时候,开始点写错。应该是从i+1开始,而代码里从i开始了

算法错误,前缀和里将不应加入的3加入了,如果没有3,前缀和对k取模将是:0 4 2 1 3 0,而我的算法使得从第三项起后面的数都是+3之后%k的,本来该匹配上的0因此变得不匹配了。因此“跨3”之后结果会错误。

到这里该发现了,我的算法错误之处在于没办法判断一个数该加进去还是不该加进去

那么,加或不加就都试试吧,正确做法应该是dp

dp就要找状态了,对每个数,状态有两种,加入sum或者不加入sum。此外还需要记录“加到第i项时不同的sum有几种,每一种的答案是多少”设计dp数组

dp[第i项][sum%k]=最大序列长度

状态转移方程:

dp[i+1][sum%k] = max(dp[i+1][sum%k],dp[i][(sum-num[i])%k]+1)//将num[i]加入

dp[i+1][sum%k] = max(dp[i+1][sum%k],dp[i][sum%k])//不将num[i]加入

num存储不能从0开始,要从1开始了,因为上面的方程i从1开始循环到n

由于k不知道多大,可以用vector来存。下面是学长的代码:

#include <bits/stdc++.h>
using namespace std;
 
const int N=100010;
int a[N];
 
void gmax(int &x,int y)
{
    if (x<y) x=y;
}
 
int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    for (int i=1;i<=n;i++) {
        scanf("%d",&a[i]);
    }
    vector< vector<int> > dp(n+1,vector<int>(k,-1e9));
    dp[0][0]=0;
    for (int i=0;i<n;i++) {
        for (int j=0;j<k;j++) {
            gmax(dp[i+1][(j+a[i+1])%k],dp[i][j]+1);
            gmax(dp[i+1][j],dp[i][j]);
        }
    }
    printf("%d\n",dp[n][0]);
    return 0;
}

抛开算法,写法的亮点有两个:

max函数的写法,传引用,如果x需要被改变,直接修改其值。

vector的初始化是在变量名后加括号。如果vector嵌套,括号里还要写vector<int>

学长们能轻易做出来的题,我要认真思考上两天。和学长的差距肥肠巨大,还需要努力呀!

猜你喜欢

转载自blog.csdn.net/krypton12138/article/details/79981243