http://acm.sjtu.edu.cn/OnlineJudge/problem/1280
题目的描述比较逗比,核心的数学问题是这样的:
一个数集M(此处集合可以认为元素之间没有互异性),有N个元素,从中取出t个元素(t!=0),使得它们的和是F的倍数。输出所有取法的个数除以1e8之后的余数。
*/
1.暴力搜索
之前在白书里学习过子集生成的几种方法,其中二进制法非常简洁,那么第一版代码就产生了。
1 #include <iostream> 2 #include <cmath> 3 using namespace std; 4 5 int main(int argc, char const *argv[]) 6 { 7 int N,F,sols=0; 8 cin>>N>>F; 9 int vals[2001] = {0}; 10 for (int i = 0; i < N; ++i) 11 { 12 cin>>vals[i]; 13 } 14 for(int i=0;i<(1<<N);i++){ 15 int tem = 0; 16 for(int j=0;j<N;j++) if(i&(1<<j)){ 17 tem+=vals[j]; 18 } 19 if(tem % F==0 and tem!=0) 20 sols++; 21 } 22 cout<<sols<<endl; 23 return 0; 24 }
Judging... PROB=1280 LANG=C++ Accepted (Time: 6ms, Memory: 4968kb) Accepted (Time: 6ms, Memory: 4972kb) Accepted (Time: 6ms, Memory: 4956kb) Accepted (Time: 6ms, Memory: 4984kb) Accepted (Time: 14ms, Memory: 4980kb) Accepted (Time: 89ms, Memory: 4956kb) Wrong Answer (Time: 6ms, Memory: 4964kb) Wrong Answer (Time: 6ms, Memory: 4956kb) Wrong Answer (Time: 237ms, Memory: 4964kb) Time Limit Exceeded (Time: 0ms, Memory: 0kb)
知道肯定通不过的啦,但是一开始很奇怪为什么789三个测试点的结果是WA,第一感觉是溢出,但是改成了unsigned long long 还是一样,后来才看到N的大小范围是1-2000 当N非常大的时候1<<N已经超乎人类想象了 unsigned long long 根本hold不住。肯定会报错。
2.回溯法+初步优化
为什么会想到回溯法呢,就是因为子集和问题还是比较容易的。子集和问题:对一个集合,取出n个元素使得他们的和等于常数C,输出所有的解决方案。
那么在这种框架下,我们只要把C设置为=kF即可,k的范围可以通过所有元素总和来确定。
初步优化:实际上如果N个元素中有一部分本身就是F的倍数的话,那么这部分数可以拿出去单独处理不用参与后续的计算,只要在最终的结果中融合进来就可以了。
回溯的条件呢,就是当前选择元素+剩余全部元素的和都没有目标和大的时候,就不要选择了,当然了还有一个明显的条件就是如果当前选择的元素的和已经超过了目标和的时候必然要回溯。
回溯法的递归法和非递归法都写了。先看一下非递归法,最终貌似没有实现第一个回溯条件,因为那部分代码加上去反而变慢了。
1 //arr是集合 n是集合的元素个数 kf是目标和 2 int traceback(int* arr,int n,int kF){ 3 //非递归的回溯法 计算所有sum = kF 的子集的个数 4 int sol = 0;//满足条件的子集的数目 5 bool visited[2001]={0};//标记该元素是否在当前正在计算的子集序列里 6 int curSum=0;//当前选的子集的和 7 int p=0;//指针 8 while(p>=0){ 9 10 //进行优化 如果 p后面的所有数字加起来 也不够 kF-curSum 11 12 if(!visited[p]){ 13 visited[p] = true; 14 curSum += arr[p]; 15 //加了这个之后 速度更慢了是怎么回事。。。。QAQ 16 // if(rest[n-1]-rest[p]+curSum < kF){ 17 // curSum += rest[n-1]-rest[p]; 18 // for (; p<n; p++) { 19 // visited[p]=true; 20 // }//此时p==n 21 // //cout<<"aloha"<<endl; 22 // } 23 if(curSum > kF){ 24 visited[p]=false; 25 curSum -= arr[p]; 26 }else if(curSum == kF){ 27 sol++; 28 //要怎样才能继续查找 假装没有来过 直接回溯到上一个被选择的点? 29 visited[p]=false; 30 curSum -= arr[p]; 31 } 32 p++;//跳过刚才那个使溢出的元素 33 34 } 35 36 //回溯 37 if(p>=n){ 38 while(visited[p-1]){ 39 p--; 40 visited[p]=false; 41 curSum -= arr[p]; 42 if(p<1) return sol;//没有解 43 } 44 while(!visited[p-1]){ 45 p--; 46 if(p<1) return sol; 47 } 48 //改变路线 49 curSum -= arr[p-1];//此时的p-1是被选择上的最后一个元素 50 visited[p-1]=false; 51 } 52 } 53 return sol; 54 }
这个7 8 9 10都是TLE。
递归法两个回溯条件都实现了。
void makeSum(int index,int sum){ if(sum==0){//满足条件,输出栈中的所有元素 cot++; return; //不断返回即可 } //index表示元素的个数 i从最后一个下标开始 向0趋近 同时要保证 前i项的和要大于目标和sum才有计算的必要 for(int i=index-1;i>=0 && sum<=rest[i];i--){ if(notoks[i]<=sum){//如果当前元素小于目标和 S.push(notoks[i]);//当前元素入栈 makeSum(i,sum-notoks[i]);//去计算除了i之后的和 S.pop();//再放弃,递归中要手动对全局变量进行栈的操作 } } }
即使这样,依然是TLE。其实做的时候就知道肯定还是TLE的,毕竟要对很多个kF进行处理,而且回溯的方法也不是很精明。
3.迷茫期
想到高中学数论时经常处理的同余问题,于是想着能不能对所有的数按照余数分类,然后统计个数,再想办法从中取出满足条件的组合,进行统计。
这有很多难点:1.如何判断一个同余的那个元素是不是已经在选择的集合里了。2.对2元子集的判断很好说,虽然多元的都可以递归为2元的,但是这里就是涉及到难点1,如何判断重复。
有一个小收获就是,抽屉原理+同余可以迅速找出一个解,当然这时必须是N>F,因为(前k项和)的余数的个数n要大于F,肯定会有重复,而余数相同的这两个数S1,S2 进行相减,那么中间序列中所有元素的和自然就是F的倍数了
4.经过风男指点之后的动态规划算法。
百度百科里的一些干货:
基本模型
(1)确定问题的决策对象。
(2)对决策过程划分阶段。
(3)对各阶段确定状态变量。
(4)根据状态变量确定费用函数和目标函数。
(5)建立各阶段状态变量的转移过程,确定状态转移方程。
状态转移方程的一般形式:
一般形式: U:状态; X:策略
顺推:f[Uk]=opt{f[Uk-1]+L[Uk-1,Xk-1]} 其中, L[Uk-1,Xk-1]: 状态Uk-1通过策略Xk-1到达状态Uk 的费用 初始f[U1];结果:f[Un]。
2.无后效性将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
3.子问题的重叠性 动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
for(int j=0;j<F;j++)//以不同的余数 为遍历条件 { sols[i][j]=(sols[i-1][j]//不选第i个数 那就用上一层的j就可以了 + sols[i-1][(F+j-tmp)%F])%int(1e8);//若是选择第i个数 那么就需要让上一层的余数加上tmp在取余之后是j 反推就是k=(F+j-tmp)%F //正推 int k = (j+tmp)%F;//把第i个数的tmp 和 前i-1个数的余数j 相加取余 //sols[i][k]=(sols[i-1][j]+sols[i-1][k])%int(1e8); }
#include <iostream> using namespace std; int main() { int N,F;//2000 int result=0; cin>>N>>F; int ok = 0,notok=0; int a[2000]; for (int i = 0; i < N; ++i) { int t = 0; cin>>t; if(t%F==0){ ok++;//元素本身是F倍数的单独进行处理 }else{ a[++notok]=t%F;//如果本身不是F的倍数 我们只需要计算它的余数 } } int sols[2001][1000]; //sols存储的是 从前i个元素 选若干元素组成的子集的数目 which满足总和为modF余j sols[0][0]=1; //初始化 前0个元素 以0为余数的解的个数肯定是1 而以其他为余数的解的个数是0 for(int i=1;i<=notok;++i)//开始向每一层里铺垫元素 { int tmp=a[i]%F;//第i个数的余数即为tmp for(int j=0;j<F;j++)//以上一层的 不同的余数 为遍历条件 { sols[i][j]=(sols[i-1][j]//不选第i个数 那就用上一层的j就可以了 + sols[i-1][(F+j-tmp)%F])%int(1e8);//若是选择第i个数 那么就需要让上一层的余数加上tmp在取余之后是j 反过来算就是k=(F+j-tmp)%F // int k = (j+tmp)%F;//把第i个数的tmp 和 前i-1个数的余数j 相加取余 // sols[i][k]=(sols[i-1][j]+sols[i-1][k])%int(1e8); } } result = sols[notok][0]-1;//结果就是前n个元素 除以f 以0为余数的解的个数 再减去全都不选(裸奔)的情况 // for (int i=0; i<=notok; ++i) { // for (int j=0; j<F; j++) { // cout<<sols[i][j]<<" "; // } // cout<<endl; // } //cout<<result<<endl; cout<<((result*(1<<ok))+(1<<ok)-1)%int(1e8)<<endl; return 0; }