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
朴素算法:选1个,选2个,选3个……选n个都试一下。复杂度过大,暴力是不可能AC的
改良算法:选n个,如果选n个可以的话直接break输出n,如果不可以试n-1,n-2……
假如出题人找到某组测试样例n很大,而答案m很小,这种算法就很有可能TLE,而敲出来这种风险代码耗费的时间显然不值得我去尝试。
所以要寻找更高效的算法:
找到这样一个博客,里面介绍了求子序列个数的方法:
对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>
学长们能轻易做出来的题,我要认真思考上两天。和学长的差距肥肠巨大,还需要努力呀!