基础搜索算法题解(A-C)

来源:互联网 发布:mac 新浪微博好慢 编辑:程序博客网 时间:2024/05/22 17:06
练习链接:http://acm.njupt.edu.cn/vjudge/contest/view.action?cid=171#overview


A题 最多约数问题
                                                时间限制(普通/Java) : 20000 MS/ 30000 MS          运行内存限制 : 81920 KByte

题目描述

   正整数x的约数是能整除x的正整数。正整数x的约数个数记为divx)。例如,1,2,5,10都是正整数10的约数,且div(10)=4 对于给定的2个正整数a<=b,编程计算ab之间约数个数最多的数。

输入

输入的第1行有两个正整数ab

输出

若找到的a和b之间约数个数最多的数是x,则输出div(x)。

样例输入

1 36

样例输出

9


题目链接:http://acm.njupt.edu.cn/acmhome/problemdetail.do?&method=showdetail&id=1203

题目分析:数论,算术基本定理的应用,加上各种牛逼的剪枝,我是没做出来,贴个代码和正解思路,大家感受一下

本题的要求是,求出一个给定区间内的含约数最多的整数。
    首先明确一下如何求一个数的约数个数:若一个数N满足:N = A1N1 * A2N2 * A3N3 * …… * AmNm,则n的约数个数为(N1 + 1) (N2 + 1) (N3 + 1) …… (Nm + 1)。这是可以用乘法原理证明的。
    最浅显的算法是,枚举区间内的每个整数,统计它们的约数个数。这个算法很容易实现,但是时间复杂度却相当高。因为题目约定区间的最大宽度可以达到10^9的数量级,相当庞大。因此,在极限规模时,时间是无法忍受的。所以,我们需要尽量的优化时间。
    分析一下枚举的过程就会发现,如果我们枚举到两个数n和m*n(m为一相对于n较大的质数),那么我们将重复计算n的约数两次。据此,我们发现了枚举效率低的根本所在。为了解决这一重复,我们可以选取另一种搜索方法——以质因子为对象进行深度搜索。
    初始时,令number := 1,然后从最小的质数2开始枚举,枚举包含一个2、两个2……n个2的情况……直至number * 2n大于区间的上限(max)。对于每种“2^k的情况”,令number := number * 2n,再枚举3的情况,然后,枚举5的情况、7的情况……方法相同。整个过程是一个深度搜索的过程。当number大于等于区间下限(min)时,我们就找到了一个区间内的数,根据前面介绍的方法,可以得到它的约数个数。所有的区间内的数的约数个数的最大值就是我们要求的目标。
    为什么这种深度搜索可以减少常规枚举过程中的重复问题呢?请看下面的一个例子
     设给定的区间为[6,30],6,18,30为区间内的数,按照常规枚举方法,计算18,30,的时候分别计算了因子6的约数个数,重复计算2次。如果使用上述所说的深度搜索方法,求这3个数的因数个数的路径有一条公共部分,2*3,这一部分只计算了一次,求18只需再乘个3,求30只需再乘个5,相对于常规枚举减少了两次计算2*3的时间。但这种深度搜索也有问题,就是number有可能是无用的,下面的分析便是对这种深搜方法进行无用数据剪枝。
    值得注意的是,我们枚举过程中得到的number可能无用的,即无论用number去乘以多少,都无法得到区间内的数。这样的number如果继续枚举下去,无疑会大大降低效率。那么,能否通过简单的判断,将其剪去呢?答案是可以的。很容易证明,如果(min – 1) div number < max div number,则区间内存在可以被number整除的数。因为,如果区间[min, max]内存在可以被number整除的数,也即是从min到max中至少有一个数能被number整除,那么区间[min – 1, max]内的数被number除得的商肯定不止一种,所以(min – 1) div number必然小于max div number。
反过来,如果(min-1)div number=max div number,则[min,max]内不存在可以被number整除的数。
证明如下:
假设[min,max]内存在可以被number整除的数
如果(min-1) div number=max div number,则min div number = max div number,那么①min等于max,且min和max均可以被number整除,或者②max>min,min可以被number整除,①的情况下可推出,(min-1) div nunber<max div number,与条件矛盾,舍去;②的情况下也可以推出(min-1) div number<max div number,舍去。所以结论成立。因此,我们只需枚举符合要求的number;至于不符合的,可以剪去

    此外,我们枚举的质数可能会达到很大。因为给出的整数最大可以达到4,000,000,000,它的质因数自然最大也可以到100,000,000的数量级。如果按上面的方法枚举,显然无法承受时间的压力。但是,我们又可以看到,对于区间内的任一整数,它包含的大于SQRT(4,000,000,000) = 2*31623的质因数最多只可能有一个。因此,我们只需枚举小于2*31623的质数。如果对一个符合要求的number(即,可以证明区间内至少存在一个数可以被number整除),无法找到一个小于2*31623的质数p使得number * p * x ∈ [min, max](x为一正整数)。那么,可知number需要乘以一个大于31623的质数才能得到number’,使得number’ ∈ [min, max]。根据前面介绍的乘法原理,只需将number包含的约数个数乘以2,即得number’包含的约数个数。
    我们还能看到,如果当前搜索状态为(from, number, total),其中from是指当前枚举到的质因子(按从小到大枚举),total是指number中包含的约数个数。那么剩下的因子数最多为q = [logfrom(max / number)],这些因子组成的约数个数(即上述求约数个数时用到的一串乘积)最大为2q。当前所能取到的(理想情况)最大约数个数就是total * 2q,如果这个数仍然无法超过当前最优解,则这一分支可以剪去。
 
    深度搜索的过程,从表面上看是一个指数级的复杂度。其实不然,更准确的说,复杂度应为O(plogn)。因为,它的指数增长速度是随n成对数级增长,本质上说,还是多项式级的算法。而且我们还进行了一些剪枝,由于枚举中的分支多数为非法的,因此通过剪枝可以去掉绝大多数的分支。这样就大大提高了程序的效率。

#include <cstdio>#include <cstring>#include <cmath>int const MAX = 100005;int prime[MAX];int ma, cnt, l, r;void get_prime()  {    bool get[MAX];    memset(get, true, sizeof(get));    get[0] = get[1] = false;    for(int i = 2; i <= sqrt(MAX); i++)        if (get[i])            for(int j = i * i; j <= MAX; j += i)                  get[j] = false;    for(int i = 2; i <= MAX; i++)        if(get[i])            prime[++cnt] = i;}void search(int from, int tot, int num, int left, int right){    ma = tot > ma ? tot : ma;      if((left == right) && (left > num))        search(from, tot * 2, num * left, 1, 1);    for(int i = from; i <= cnt; i++)        {        if (prime[i] >right)               return;        else        {            int j = prime[i], x = left - 1, y = right, n = num, t = tot, m = 1;            while(true)            {                m ++;                   t += tot;                x /= j;                y /= j;                if (x == y)                    break;                n *= j;                search(i + 1, t, n, x + 1, y);            }            if (tot < (ma / (1 << m)))                return;        }   }}int main(){    cnt = 0;    get_prime();    while(scanf("%d %d", &l, &r) != EOF)    {        if((l == 1) && (r == 1))            ma = 1;        else        {            ma = 2;            search(1, 1, 1, l, r);        }        printf("%d\n", ma);    }}



 

B题 Square

 

Time Limit: 3000MSMemory Limit: 65536KTotal Submissions: 19982Accepted: 6955

 

Description

Given a set of sticks of various lengths, is it possible to join them end-to-end to form a square?

Input

The first line of input contains N, the number of test cases. Each test case begins with an integer 4 <= M <= 20, the number of sticks. M integers follow; each gives the length of a stick - an integer between 1 and 10,000.

Output

For each case, output a line containing "yes" if is is possible to form a square; otherwise output "no".

Sample Input

34 1 1 1 15 10 20 30 40 508 1 7 2 6 4 4 3 5

Sample Output

yesnoyes

Source

Waterloo local 2002.09.21

 

 

题目链接  :http://poj.org/problem?id=2362

 

题目大意  :有n个木条,问用这n个木条能不能拼出一个正方形

 

题目分析  :DFS+剪枝,这题是POJ 1011 那题的简单版,而且这题时间放的很宽,基本上小剪一下就可以过,笔者本着学习的态度写了不少剪枝,依旧跑了110ms,不知0ms大神是怎么做到的,若有大神知道还求指点

1.总长必须是4的倍数,否则无解

2.对木条从大到小排序,如果最长边大于总长的四分之一则无解,而且每次从长边开始选可以减少判断次数,因为长边的灵活度比较低,如果有一根长边用不上则无解

3.搜索时如果两根木条的长度相同,前一根没有选则后一根必然不会选

4.每次加之前判断加上是否会超过边长,若会超过则不选


#include <cstdio>#include <cstring>#include <algorithm>using namespace std;int const MAX = 21;int stick[MAX];int vis[MAX];  //标记木条是否用过int num, side;  //num表示木条数,side表示边长bool flag;//cur表示当前正在拼装的木条长度,other表示其他的木条,count表示拼成的边数bool cmp(int a, int b){    return a > b;}bool DFS(int cur, int other, int count){    //如果当前长度等于边长,count加1,当前长和其他都置为0开始拼下一条边    if(cur == side)    {        count++;        cur = 0;        other = 0;    }    //如果count等于4,说明找到可行解,返回true    if(count == 4)        return true;    for(int i = other; i < num; i++) //枚举其他木条    {        //若两个木棒长度相同,前一个没用,则这一个也不会用        if(i && !vis[i-1] && stick[i] == stick[i-1])            continue;        //如果下一根未用且加上后长度小于边长        if(cur + stick[i] <= side && !vis[i])         {            vis[i] = 1;  //标记为已使用            //如果当前方案能找到可行解则返回true            if(DFS(cur + stick[i], i + 1 , count))                return true;            //如果找不到,将改木条标记为未使用,供下一种方案使用            vis[i] = 0;        }    }    return false;}int main(){    int n, sum;    scanf("%d",&n);    while(n--)    {        flag = false;        sum = 0;        memset(vis,0,sizeof(vis)); //木条初始化为未用        scanf("%d",&num);        for(int i = 0; i < num; i++)        {            scanf("%d",&stick[i]);            sum += stick[i];        }        sort(stick, stick+num, cmp); //将木条从小到大排序        //如果总长不是4的倍数或者最长边大于总长的四分之一则不可能拼成        if(sum % 4 || stick[0] > sum / 4)        {            printf("no\n");            continue;        }        side = sum / 4;        flag = DFS(0,0,0);        if(flag)            printf("yes\n");        else            printf("no\n");    }}




                                                          C题 Sticks


Time Limit: 1000MS
Memory Limit: 10000KTotal Submissions: 120211
Accepted: 27813

Description

George took sticks of the same length and cut them randomly until all parts became at most 50 units long. Now he wants to return sticks to the original state, but he forgot how many sticks he had originally and how long they were originally. Please help him and design a program which computes the smallest possible original length of those sticks. All lengths expressed in units are integers greater than zero.

Input

The input contains blocks of 2 lines. The first line contains the number of sticks parts after cutting, there are at most 64 sticks. The second line contains the lengths of those parts separated by the space. The last line of the file contains zero.

Output

The output should contains the smallest possible length of original sticks, one per line.

Sample Input

95 2 1 5 2 1 5 2 141 2 3 40

Sample Output

65

Source

Central Europe 1995

题目链接  :http://poj.org/problem?id=1011

题目大意  :给若干根切断的木条,用完全部所给木条拼成n根等长的原始木条,求所拼原始木条的最短长度

题目分析  :DFS + 剪枝 ,本题限制时间为1000ms,剪枝不到位绝对超时,下面列出几条不可少的剪枝
1. 设有n根木条枚举能拼成的长度为sum/n~1,这里n必须是sum的约数否则无解,找到了则为最优解,因为均分的越多长度就越短
2.按长度从大到小排序,最长的一根长度必然小于sum/n,否则无解,从长的开始取,因为长的灵活度比较低,之后便于剪枝。
3.若搜索时某两根的长度相同,第一根没取那第二根也不会取
4.每次取的木条加上取之前的长度要小于sum/n
5.之前的POJ2362  http://blog.csdn.net/tc_to_top/article/details/38460259  没加这个剪枝跑的110ms加上后0ms飘过,这题则必须加这条剪枝 :如果当前长度cur为0,当前取的最长木条为stick[i],结果stick[i]拼接失败则我们可以直接退出搜索,当前情况无解,因为如果某次当前最长的木条没被选上,则之后木条数更少了它更不会被选上,因为我们是按照降序排列的

这里再给一组数据: 
64
40 40 30 35 35 26 15 40 40 40 40 40 40 40 40 40 40 40 40 40 40 
40 40 43 42 42 41 10 4 40 40 40 40 40 40 40 40 40 40 40 40 40 
40 25 39 46 40 10 4 40 40 37 18 17 16 15 40 40 40 40 40 40 40 40
答案: 454
这组数据用我的0ms过的代码跑了3,4秒,只能说POJ数据略水,该组数据要有1秒内过的求教

#include <cstdio>#include <cstring>#include <algorithm>using namespace std;int const MAX = 64;int vis[MAX];int stick[MAX];int num, one, div_num;bool cmp(int a, int b) {    return a > b;}//cur代表当前某根木条的拼接长度,other代表未用木条,count代表拼成的根数bool DFS(int cur, int other, int count){if(count == div_num) //若拼成的根叔等于份数,则找到返回return true;for(int i = other; i < num; i++){//若当前供选长度与前一个相等且前一个未使用则不考虑当前这根if(i && !vis[i-1] && stick[i] == stick[i-1])continue;if(cur + stick[i] <= one && !vis[i]){//若加上小于原始长度将其标记为已使用继续搜索if(cur + stick[i] < one){vis[i] = 1;if(DFS(cur + stick[i], i + 1, count))return true;//若之前方案不可行,将当前这根标记为未使用vis[i] = 0;//这是个很重要的剪枝,若当前选择的最长长度得不到解,该方案必然无解if(cur == 0)return false;}//若加上等于原始长度将其标记为已使用拼成一根,开始拼下一根if(cur + stick[i] == one){vis[i] = 1;if(DFS(0, 0, count + 1))return true;//若之前方案不可行,将当前这根标记为未使用vis[i] = 0;//这是个很重要的剪枝,若之前的DFS(0,0,count+1)有一个不满足//则该方案必然无解return false;}}}return false;}int main(){int  sum;while(scanf("%d",&num) != EOF && num){sum = 0;memset(vis,0,sizeof(vis));for(int i = 0; i < num; i++){scanf("%d",&stick[i]);sum += stick[i];}sort(stick, stick+num, cmp);  //从大到小排序 //从份数最多的枚举起,则找到一组解就可以退出,因为它必为最短for(int i = num; i > 0; i--){if(sum % i == 0)   //每根原始木条的长度必须是整数{one = sum / i;  //一根原始木条的长度//若当前的原始木条短于最长的切断的木条长度则原始木条值非法if(stick[0] > one) continue;else{div_num = i; //份数if(DFS(0,0,0)) //找到一个可行解就退出break;}}}printf("%d\n",one);}}




0 0