部分算法习题

来源:互联网 发布:淘宝主播雪欧尼男朋友 编辑:程序博客网 时间:2024/05/22 04:42

1.将这组数据分成两个集团,使得两个集团中数据之和相差最小,输出最终两个集团中的数据之和 
 如例子中的这个分组应该为:33 40和35
即输出为:40  68。
这可以看成是一个典型的背包问题
这题背包的容量相当于总价值的一半,而物品的体积和价值单位都是1,每个物品的价值=物品的体积=数的大小
背包问题就是说现在有一个容量为s的背包,现在给你n个物品,其体积和价值分别为wi,vi,那么求在容量s内能获得的最大价值.  我们用d[i]数组来保存当背包容量为i时,背包的容纳最大价值。

这可以采用动态规划来作。为了设计一个动态规划算法,需要推导一个递推关系,用较小子实例的解的形式来表示背包问题的实例的解。让我们来考虑一个由前i个物品(1≤i≤n)定义的实例,物品的重量分别为w1, …,wi、价值分别为v1,…,vi,背包的承重量为j(1≤j≤W)。设V[i,j]为该实例的最优解的物品总价值,也就是说,是能够放进承重量为j的背包中的前i个物品中最有价值子集的总价值。可以把前i个物品中能够放进承重量为j的背包中的子集分成两个类别:包括第i 个物品的子集和不包括第i个物品的子集。然后有下面的结论:
    1.根据定义,在不包括第i个物品的子集中,最优子集的价值是V[i-1,j]。
    2.在包括第i个物品的子集中(因此,j-wi≥0),最优子集是由该物品的前i-1个物品中能够放进承重量为j-wi的背包的最优子集组成。这种最优子集的总价值等于vi+V[i-1,j-wi]。
因此,在前i个物品中最优解的总价值等于这两个价值中的较大值。当然,如果第i 个物品不能放进背包,从前i 个物品中选出的最优子集的总价值等于从前i-1个物品中选出的最优子集的总价值。
初始条件:当j≥0时,V[0,j]=0,我们的目标是求V[n,W]。

这个问题的答案1:
#define len 64*400
int main()
{
    int a[len],d[len],i,j,n,sum;
    while(scanf("%d",&n)==1)
    {
          memset(d,0,sizeof(d));  //初始化条件,全为0
          for(i=0,sum=0;i <n;i++) 
          {
              scanf("%d",&a[i]);
              sum+=a[i];    
          }
          for(i=0;i <n;i++)
          {
              for(j=sum/2;j >=a[i];j--) 
              {
                  if(d[j-a[i]]+a[i] >d[j])
                        d[j]=d[j-a[i]]+a[i];    
              }
          }    
          printf("%d %d/n",d[sum/2],sum-d[sum/2]);
    }
    return 0;    
}

2.int Fbag(int n,int weight,int *w)
{
    int a,b;
    if(n==0)
    {
        if(weight>=w[0]) return w[0];
        else return 0;
    }
    if(weight>=w[n])
    {
        a=Fbag(n-1,weight-w[n],w)+w[n];
        b=Fbag(n-1,weight,w);
        if(a>b) return a;
        else return b;
    }
    return Fbag(n-1,weight,w);
}

int main()
{
    int n,sum_weight(0),bag_weight,fin_weight;
    cin>>n;
    int *arr=new int[n];
    for(int i=0;i<n;++i)
    {
        cin>>arr[i];
        sum_weight+=arr[i];
    }
    bag_weight=sum_weight/2;//最接近所有重量的一半为最优解
    fin_weight=Fbag(n-1,bag_weight,arr);//最终一个背包的重量
    cout<<"group A:"<<sum_weight-fin_weight<<endl;
    cout<<"group B:"<<fin_weight<<endl;
    return 0;
}

 

2. 输入包含两个整数m和n,并且m<n。输出一个由m个随机数字组成的有序列表,这些随机数的范围是0...n-1,并且每个整数最多出现一次。就概率上而言,我们希望得到不需要替换的选择,其中每个选择出现的可能性都相同。

解决方法一:
int bigrand()
{
 return RAND_MAX*rand() + rand();
}
/*
**函数的伪码为:
**select = m
**remaining =n
**for i = [0,n)
**      if(bigrand() % remaining) <select
**           print i
**           select--
**     remaining--
**只要m<=n,程序就选出m个整数:不能选择更多的整数,因为select变为0时不能选择更多的整数;并且选择的整数
**也不可能少于m,因为当select/remaining变为1时总会选中一个整数。
**这种解决方案当n非常大时,该代码可能非常慢
*/
void genknuth(int m, int n)
{
 for(int i = 0; i<n; i++)
 {
  if(bigrand()%(n-i)<m)
  {
   cout <<i<<"/n";
   m--;
  }
 }
}

int main()
{
 srand((unsigned)time(NULL));
 int m,n;
 cout<<"Please enter m and n:";
 cin>>m>>n;
 genknuth(m,n);
 return 0; 
}
这个方法有一个很大的漏洞,当m=0时,程序就死掉

解决方法二:就是在一个初始为空的集合中插入随机整数,直到足够的整数。
伪代码如下所示:
initialize set S to empty
size = 0
while size < m do
      t = bigrand()%n
      if t is not in S
           insert t into S
           size++
print the elements of S in sorted order
该算法在选择元素时能够保证所有的元素都具有相同的选中概率,它的输出是随机的
void genknuth(int m, int n)
{
 set<int> S;
 while( S.size() < m)
  S.insert(bigrand()%n);
 set<int>::iterator i;
 for(i = S.begin(); i != S.end(); ++i)
  cout<<*i<<"/n";
}
C++标准模板库规范能够保证在O(log m)时间内完成每个插入操作,集合内部迭代需要O(m),所以整个程序需要的时间为O(m log(m)(和n相比,m较小时)。然而,该数据结构的空间消耗较大。

解法三、
for i = [0,n)
      swap(i, randint(i,n-1))
这个是弄乱数据x[0...n-1]。在这个问题中,只需要搅乱数组前面m个元素
int bigrand()
{
 return RAND_MAX*rand() + rand();
}

int randint(int l, int u)
{
 return l + bigrand() % (u-l+1);
}

void genknuth(int m, int n)
{
 int i,j;
 int *x = new int[n];
 for(i = 0; i < n; i++)
  x[i] = i;
 for(i = 0; i < m; i++)
 {
  j = randint(i,n-1);
  swap(x[i],x[j]);
 }
 sort(x, x+m);
 for(i=0; i<m; i++)
  cout<<x[i]<<"/n";
}

int main()
{
 srand((unsigned)time(NULL));
 int m,n;
 while(1)
 {
  cout<<"Please enter m and n:";
  cin>>m>>n;
  genknuth(m,n);
 }
 return 0; 
}
该算法使用了n个字的内存并需要O(n+mlogm)时间
解法四:(递归)
void randselect(int m,int n)
{
    assert(m>=0 && m <= n);
    if(m>0)
    if((bigrand()%n) < m)
    {
        randselect(m-1,n-1);
        cout<<n-1<<",";
    }
    else
          randselect(m, n-1);
}

3.一道ACM题 引发的思考 

fans 是一名数学天才,大家都这么说。天才 fans 的两大最新发现如下:
(1)正整数 n 除 3 的余数,等价于,正整数 n 的各位数字之和除 3 的余数;
(2)正整数 n 除 9 的余数可以通过这样的方法来计算:
          计算 n 的各位数之和,设为 m,如果 m 已经是一位数,那么余数就是 m;
          否则设 n = m,重新进行计算n的各位数之和 m,
          直到 m 是一个一位数。
          但是,正整数除1,2,4,5,6,7,8,也存在类似的性质吗?这真是一个难题啊!
fans 想睡觉了,不去管了。现在请你计算一下正整数n除一位数 m 的余数。 
文件中有一些数对,一为大整数(可能大到100位)n,另一为一位数 m( > 0)。
求其 n 除以 m 的余数。

Sample Input:

23 7
123 9

Sample Output:

2
6
 
紧跟着就有人回帖,给出一个几近完美的答案:
 
1 余数始终为 0
2 10%2=0,偶数为0;奇数为1
3 10%3=1,各位数数字累加再取3的余数
4 100%4=0,故取末两位数对4的余数
5 10%5=0,故取个位数对5的余数
6 分别计算对2、3的余数,再孙子定理
7 10^6%7=1,故从后向前每6位分段累加再取7的余数
8 1000%8=0,故取末三位数对4的余数
9 10%9=1,各位数数字累加再取9的余数

其中比较麻烦的是7,其中对7也可这么计算:
因1000%7=-1,可从后向前每三位分段,再奇偶编号分别累加,代数和后再取7的余数
 
对,最麻烦的是7,他的方法不好
有改进的方法吗?有,灵光一闪,灵感来自于21 和 63
为什么是这两个数?我也不知道。
 
下面是我的回复:
 
这里关键的问题是求7的余数,别的问题都好说。
我介绍一个快速算法。
先看21可以被7整除,有什么特性呢?
将个位的数乘以2与原来数的乘除10的商相减(我们定义为步骤A)
就是 2 - 1*2 = 0 可以整除
63也是同样道理。

我们再举一个不整除的例子。
24 / 7      - >        2 - (4*2) = -6      - >     1    但实际余数 为 3
36 / 7      - >        3 - (6*2) = -9         - >     5    但实际余数 为 1
这样我们就可以建立一个数组 名叫 modByA = {0, 3, 6, 2, 5, 1, 4}
查表就可以了。

还有一个例子 1481 它对7的余数为 4
但用这个方法,我们看看结果
1481 - >     148 - (1 * 2) = 146  - >      14 - (6*2) = 2
modByA[2]对应的是6并不正确
怎么回事?
原因很简单 你用了两次步骤A, 所以要查表两次
于是乎,再找modByA[6]的对应 就是 4

我们再看一个更长的例子

12345345 mod 7 = 5
1234534 - ( 5*2) = 1234524
123452 - (4 *2) = 123444
12344 - (4 *2) =12336
1233 - (6*2) = 1221
122 - 1*2 = 120
12 -0*2 = 12
1 - 2*2 = -3 - > 4

modByA[4] 对应的是5 OK
不是应该查7次吗?
对啊? 7次一个循环,查一次就好了。
查表次数的规律
1 2 3 4 5 6 1 2 3 4 5 6.....
所以,最多查6次。
4.
给定一个无序整数序列,怎样求其最长递增子序列?

#include <deque>
#include <cassert>
void MaxIncreaseQueue(const std::deque<int> & source,std::deque<int> & result)
{
    typedef std::deque<int> IntArray;
    //怎样从source中得到最长递增子序列放到result中?
}

经过分析我们发现,首先,我们要得到这个序列的最小值,所以我们建立了我们的第一个类GetMinValue:

class GetMinValue
{
 int minvalue_;
public:
 explicit GetMinValue(const IntArray & in){
  assert(in.size() != 0);
  IntArray::const_iterator i = in.begin();
  for(minvalue_ = *i,++i;i != in.end();++i){
   if(minvalue_ > *i)
    minvalue_ = *i;
  }
 }
 ~GetMinValue(){}
 operator int() const{
  return minvalue_;
 }
};

可以看到,GetMinValue以一个IntArray 为参数构造,并由operator int() const来返回其最小值。这样,我们可以这样来得到source的最小值:

 

 

class GetMinValue
{
 int minvalue_;
public:
 explicit GetMinValue(const IntArray & in){
  assert(in.size() != 0);
  IntArray::const_iterator i = in.begin();
  for(minvalue_ = *i,++i;i != in.end();++i){
   if(minvalue_ > *i)
    minvalue_ = *i;
  }
 }
 ~GetMinValue(){}
 operator int() const{
  return minvalue_;
 }

};

可以看到,GetMinValue以一个IntArray 为参数构造,并由operator int() const来返回其最小值。这样,我们可以这样来得到source的最小值:

 

int min = GetMinValue(source);

然后我们开始考虑算法的问题。我想很多人都知道计算机里的堆的概念,一个堆就是一个类似于树的结构,只是它的节点满足一定的排列规则,于是比一般的树更有规律,也更复杂。我们这里构造这样一个堆(同时也是一棵树,所以我用了Tree这个名字),其父节点的值总比子节点小(或相等),其右节点的值总比左节点小,如图:

                   2   ——父节点值比子节点小
                  / /                                   
                 6   3 ——右节点值比左节点小

于是,当我们遍历整个序列的时候,就可以根据以上规则建立这个堆,然后根据深度最大的节点,回溯出整个子序列。第一步我们建立节点的定义: 首先看到Node的数据成员,有value_——节点的值,length_——节点的深度(也就是子序列的长度),parent_——父节点的指针(用于回溯),rightchild_——最右的子节点的指针,leftbrother_——左兄弟的指针。为什么要用rightchild_和leftbrother_而不用几个children_呢?显然大家都能猜到,因为这个树并不一定是二叉树,所以子节点数是不确定的。
然后大家也许会注意到Node的构造函数,必须要提供父节点的指针,这只是一个设计上的问题。我想把所有关于Node指针的操作全放在Node里面,这样就不用把parent_,rightchild_等定义为public,或提供一个修改的接口。
随后是Node的析构函数,2个delete可能会吓坏许多有良好C++编程风格的高手。我只能说,当我把this赋值给parent->rightchild_(Node构造函数里)的时候,就注定了“这样的结局”(^_^)
最后是成员的访问函数,我想应该不会有多少异议。
当构造好了节点,我们开始正式构造我们的树(堆)了。树的根root_的value_总是序列里面最小的值(这就是我们定义GetMinValue的原因),这是在树的构造函数里保证的,为的是让所有可能的序列都能在这个根下生长。而当我们每向树里加入一个值为val的节点时,都要调用AddValue(int val)函数,这个函数进一步调用AddValue(Node * parent,int val)函数的AddValue(&root,val)版本。
AddValue(Node * parent,int val)函数是递规调用的,它的逻辑是这样:
如果parent不是一个节点或者val比parent的value_值小,那么返回false,告诉调用者不能插入节点;否则对parent的每一个子节点,如果其value_值比val小,那么应该对其每一个都调用AddValue;如果没有一个子节点能调用AddValue成功的话,就把val当成parent的子节点插进来。
这个规则看似复杂,其实如果有一个流程图来表示,也许会明了得多,可惜我不会在这里画图。
现在我想我们应该来看看代码了: 大家也许会发现成员变量里的max_length_node_,这是一个追踪深度最大的节点的变量,初始化为&root_,方便最后的回溯。
函数AppendNode是真正插入节点的函数,通过一个简单的Node(parent,val)就完成了所有的动作,这就是我前面对Node的构造函数满意的地方。然后是检查深度最大的节点的变化情况,这一点应该是显而易见的。
Tree的构造函数主要用来初始化root_,要使得root_.value_的值是最小的,理由我已经说过了。析构函数更是简单得没有明显的代码。
最后是2个接口InputIntArray和OutputIntArray分别用来输入源序列和输出结果序列。注意out.push_front(p->Value())这句,因为我是从最深的子节点向上回溯的,也就是说按照相反的顺序得到最大子序列,所以用了push_front函数,这也是我使用deque而不用vector的原因。

class Node
{
    int value_,length_;
    Node *parent_,*rightchild_,*leftbrother_;
public:
    Node(Node * parent,int value):value_(value),length_(1),parent_(parent),rightchild_(0),leftbrother_(0){
        if(parent_ != 0){
            length_ = parent->length_ + 1;
            leftbrother_ = parent->rightchild_;
            parent->rightchild_ = this;
        }
    }
    ~Node(){
         if(rightchild_)
             delete rightchild_;
         if(leftbrother_)
             delete leftbrother_;
    }
 //Functions for member access
    int Value() const{
        return value_;
    }
    int Length() const{
        return length_;
    }
    Node * Parent() const{
        return parent_;
    }
    Node * RightChild() const{
        return rightchild_;
    }
    Node * LeftBrother() const{
        return leftbrother_;
    }
};

 

class Tree
{
    Node root_,*max_length_node_;
    void AddValue(int val){
        AddValue(&root_,val);
    }
    bool AddValue(Node * parent,int val){
        if(!parent || val < parent->Value())
            return false;
        int count = 0;
        for(Node * p = parent->RightChild();p != 0;p = p->LeftBrother(),++count){
            if(!AddValue(p,val))
                break;
        }
        if(!count)
            AppendNode(parent,val);
        return true;
    }
    void AppendNode(Node * parent,int val){
        Node * p = new Node(parent,val);
        if(p->Length() > max_length_node_->Length())
            max_length_node_ = p;
    }
public:
    explicit Tree(int value):root_(0,value),max_length_node_(&root_){}
    ~Tree(){}
    void InputIntArray(const IntArray & in){
        for(IntArray::const_iterator i = in.begin();i != in.end();++i)
            AddValue(*i);
    }
    void OutputIntArray(IntArray & out){
        out.erase(out.begin(),out.end());
        Node * p = max_length_node_;
        while(p != &root_){
            out.push_front(p->Value());
            p = p->Parent();
        }
    }
};

 

所有的辅助工具都介绍了,现在你也许要不耐烦的问:“说了这么多,到底怎么得到最大子序列的呢?”那么现在让我们回到最初的MaxIncreaseQueue函数: 一切就OK了!
如果你对上面的4行代码还有疑问的话,请参阅我前面的一大堆“口水”。

void MaxIncreaseQueue(const std::deque<int> & source,std::deque<int> & result)
{
    Tree t = Tree(GetMinValue(source));
    t.InputIntArray(source);
    t.OutputIntArray(result);
}

 

int main()
{
    typedef deque<int> IntArray;
    const int size = 30;
    int array[size] = {5,4,9,4,6,1,7,4,3,5,7,5,3,2,3,4,5,8,1,6,5,4,0,0,1,0,0,0,0,0};
    IntArray source(array,array + size),result;
    MaxIncreaseQueue(source,result);
    cout<<"Max Queue:";
    for(IntArray::const_iterator i = result.begin();i != result.end();++i)
        cout<<*i<<' ';
    cout<<'/n';
    return 0;
}

经过分析我们发现,首先,我们要得到这个序列的最小值,所以我们建立了我们的第一个类GetMinValue: 可以看到,GetMinValue以一个IntArray 为参数构造,并由operator int() const来返回其最小值。这样,我们可以这样来得到source的最小值:
5、当m接近n时,基于集合的算法会产生很多集合中早就存在的整数,因此需要去掉这些整数。写一个算法,即使是在最差情况下,该算法也只需要m个随机数值?
void genfloyd(int m, int n)
{
     set<int> s;
     set<int>::iterator i;
     for(int j = n-m; j < n; j++)
     {
           int t = bigrand() % (j+1);
           if ( S.find(t) == S.end())
                 S.insert(t);             //t not in S
           else
                 S.insert(j);             //t in S
     }
     for(i = S.begin(); i != S.end(); ++i)
          cout<<*i<<"/n";
}