【算法学习笔记】23.动态规划 解题报告 SJTU OJ 1280 整装待发

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 }
View Code

 

这个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]。

需要满足的条件:
1.最优化原理(最优子结构性质) 最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
2.无后效性将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
3.子问题的重叠性 动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。


那么根据句这8个条件来分析一下这个题目。
1.确定问题的决策对象。其实我觉得这一步非常难的,要把一个问题用动态规划的视觉去观察才能达成这一步。我们要把问题换成一种描述,最重要的一步就是把目标换一种描述。『问有多少种方案可以从N个数里选出一些数且和为F的倍数』===》『有多少种方案可以从前N个数中选出一些数且这些数的和除以F的余数是0

这种表述方法的特点是,把一个单独的终极性的问题,改成了可以进行扩展的,貌似可以递归的问题。问的是前N个数,余数为0的方案数,那么前N-1个数,余数为0的方案数对答案有帮助么?仔细想一想,是有的,假设前N个数以j为余数的方案数分别是sols[n-1][j],假设第N个数的余数是tmp,分析可知最终的答案的计算可以分为两部分。
第一部分:不选择第N个数为当前子集,那么就是mod[n-1][0]
第二部分:如果选择第N个数为当前子集,那么就是mod[n-1][F-tmp],因为只有这样才能保证之前的方案加上第N个数之后余数恰好为0

2.对决策划分阶段,根据刚才的深入分析,我们可知,如果要得到结果,那么一定要存储前i个数的以j为余数的方案数。阶段/层的概念其实就是前i个数的F个状态。

3.对个阶段确定状态变量。状态变量就是,当前层以j为余数的不同子集的个数。
4.费用函数,目标函数。这好像没有涉及到。
5.确定状态转移方程 
        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);
        }    

 

最后只要输出sols[n][0]-1就可以了,减1是为了去除空集。

再分析一下,是否拥有最优子结构?核心是:一个最优化策略的子策略总是最优的。确实是这样的,每个阶段的0,都是最优的,所以符合。
无后效性,符合。整个二维数组的状态集合里,每一个状态都是对之前阶段的状态的历史的总结。
子问题的重叠性:符合,状态转移方程都写出来了还说什么=,=

总而言之,DP给我留下的第一个印象就是二维数组,每一行是一个阶段 每一个点是一个状态,每一个状态都是由前面的状态得到的。每一层和前一层的状态之间的变化关系就是传说中的状态转移方程。而只要找到这个就OK了~

完整代码://进行了局部优化 但是只比别人快了5ms而已
#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;
}
View Code

 




 




 

posted @ 2015-03-30 14:42  雨尘之林  阅读(437)  评论(0编辑  收藏  举报