第一章:算法设计基础(思维体操+常见策略+高效算法+动态规划)

  第一章:算法设计基础

1.1思维的体操

例1、例2是说了下贪心问题。(AC)

例3:分金币(代数分析) (AC)

问题描述:

  一个圆桌旁围坐n个人,没人有一定数量的金币,问怎样给金币,可以使得每个人手中金币数量相等,且被转手的金币总数量最小。

解题思路:

  设置了一些变量,最终将问题改变成了求解:给定数轴上n个点,找出一个到它们的距离之和尽量小的点。

  这类问题的最优解即取这些数的“中位数”。

例4:墓地雕塑(寻找突破口,特殊性)

问题描述:

  需要将m个新雕塑加入到n个原有雕塑中,仍使得n+m个雕塑均匀分布,求移动的总距离的最小值。

解题思路:

  发现一个共同特点,原有的n个雕塑中肯定有一个不需要移动,那么在给定雕塑数量后,每个雕塑的位置就可以确定下来了!

  收获:寻找突破口,发掘条件,由已知信息去引出一些必要的结论来帮助结题!

例5:蚂蚁(观察问题的特殊性,找其特殊点)

问题描述:

  一根木棍上有n只蚂蚁,输出t秒后每只蚂蚁的状态位置。

要点:

  (1)两蚂蚁“碰撞” == “对穿而过”;

  (2)蚂蚁之间碰撞之后的相对位置是不变的!----这个特点是题目要求中按照原先顺序输出状态的关键!

例6:立方体成像:

 

 

1.2问题求解、常见策略:----这里主要强调了一种将实际问题建模的重要性!定义适当的数据结构,且将实际问题数字化!

例7:偶数矩阵(优化枚举)

问题描述:

  给定01矩阵,改变尽量少的0为1,使得该矩阵每个元素的上下左右4个元素之和为偶数。输出改变元素的最小个数。

解题要点:

  (1)全部枚举,枚举量过大;则采用枚举部分+计算其余部分的方式;

  (2)此方法实现的前提:需要枚举量之间存在约束条件(题目要求,本题的是使得每个元素的上下左右4个元素之和为偶数),可以被我们使用,来计算其余量。

例8:彩色立方体(建模,为正方体每面编号,枚举列出每种反转情况+将问题数字化)

 

例9:中国麻将(建模,将问题数字化+枚举+递归判断是否“听牌”)

 

例10:正整数序列(寻找问题的特殊性质,细心+耐心)(AC)

问题描述:

  给定整数n,使用最少的操作次数把序列1、2....n中所有的数字变成0,每次操作可以选择多个整数,同时减去一个相同大小的正整数。

解决:

  f(n)=f(n/2)+1;  数据不强,直接递归求解即可。(下面是递归函数代码,数据量大的情况下改为递推即可。)

int f(int n){
   return n==1 ? 1:f(n/2)+1;
}

 

例11:新汉诺塔问题:

 

例12:组装电脑:

问题描述:

  你有b元钱,需要组装一台电脑。给出n个配件的各自种类、品质和价格,要求每种类型的配件各买一个,总价格不超过b,且“品质最差配件”的品质因子应尽量大。

解决:

  要求品质因子,题目中描述说明品质因子最大为 1e9,这里采用2分的方法寻找结果。

  总的来说本题是采用的: 预处理+二分枚举+贪心。

 

例13:派

 

例14:填充正方形(AC)

问题描述:

  向矩阵中填充大写字母。

解决:

  贪心实例。(因为26个字母在本题中肯定使用不完,顾不存在回溯的情况,一遍贪心即可。)

 

例15:网格(建模,客户端服务端的节点---> 网状图)

问题描述:

  n个节点,有服务器,有客户端。 叶子节点均为客户端,现在需要在服务器上安装一个程序以便于服务所有的客户端,问最少安装的数目。

解决技巧:

(1)无根树转化为有根树;

(2)贪心的策略;

(3)深搜最深节点+广搜“覆盖”节点!

 

例16:长城守卫

 

1.3高效算法设计举例

 例17:年龄排序(计数排序)(AC)

问题描述:

  给定若干居民的年龄,将他们按照从小到大的顺序输出。

解决方法:

  计数排序的方式。

高效之处: 计数排序。约束条件,每一个数字不能过大,不能超过可开数组的范围。

 

例18:开放式学分制(维护最大值)(AC)

问题描述:

  给一个长度为n的整数序列,找出两个数Ai和Aj(i

问题解决:

 法一:最简单的一种方法是二重循环(O(n^2))

 法二:枚举的同时,做部分处理,因为每次对于Aj的比较,只需要知道其前j-1个数中的最大值即可,其余的比较都是浪费的。

    本题是维护前j个数中的最大值max,可以减少一重循环,有效降低复杂度。(O(n))

 

例19:计算机谜题(维护最大值)

问题描述:

  一个计算机,输入k,将其平方,每次计算机只显示其前n位,之后前n位再平方,直到出现循环,问期间产生的n为数字中,最大的为多少。

解决:

  模拟运算过程即可,可以利用一个C++中的STL集合,简化代码。

 

例20:流星(建模!!求解时间,围绕时间t来进行了预处理,改变问题求解形式,看透问题本质!)

 问题描述:

  给你一个矩形计算机,还有n个流星的初始位置和速度求能找到流星最多的时刻。

问题解决:

  首先将每个流星出现在矩形相机中的时间段求出来,这里按照开区间显示(题目要求流星在相机边界时不算被拍到)。

  之后按照每个流星刚出现的时间以及刚刚划过相机界面的时间存入一个数组中,之后进行排序,从左到右遍历,维护一个最大值即可。

  (因为每次都是累加累减的,顾可以使用前面的技巧,只是维护信息,而不进行重新计算。)

 代码:

  

#include
#include<string.h>
#include 
#include 
#include 
using namespace std;
const int N=100010;
//更新每颗流星的坐标为一个时间区间;
int updata(int x,int a,int w,double &L,double &R){
    if(a==0){
        if(x<=0 || x>=w) R=L-1;
    }
    else if(a>0){
        L=max(L,-(double)x/a);
        R=min(R,(double)(w-x)/a);
    }
    else{
        L=max(L,(double)(w-x)/a);
        R=min(R,-(double)x/a);
    }
}
struct Event{
    double x;
    int type;
    bool operator < (const Event &a) const{
        return x a.type);
    }
}events[N*2];
int main()
{
    int i,T;int w,h,n;int x,y,a,b;
    freopen("in.txt","r",stdin);
    scanf("%d",&T);
    while(T--){
        scanf("%d %d",&w,&h);
        scanf("%d",&n);int e=0;
        for(i=0;i){
            double L=0,R=1e9;
            scanf("%d %d %d %d",&x,&y,&a,&b);
            updata(x,a,w,L,R);
            updata(y,b,h,L,R);
            if(R>L){
                events[e++]= (Event) {L,0};
                events[e++]= (Event) {R,1};
            }
        }
        sort(events,events+e);
        int num=0,ans=0;
        for(i=0;i)
            if(events[i].type==0) ans=max(ans,++num);
            else num--;
        printf("%d\n",ans);
    }
    return 0;
}
View Code

 

例21:子序列

问题描述:

  有n个正整数组成一个序列,给定整数S,求长度最短的连续序列,使它们的和大于或等于S。

问题解决:

  方法:和开放式学分制一样,最直接的思路是二重循环,枚举子序列的起点和终点。 对于这个题目来说复杂度为O(n^3)。

  优化1:降低时间复杂度,对于求和数列来说,有一个常见的前缀和技巧。令Bi=A1+A2+...Ai,可以在O(1)的时间内算出子序列的值,时间复杂度降为O(n^2)。

  优化2:但是因为此题n最大为10^5,O(n^2)的复杂度也有些慢。

    我们发现,只要是同时枚举起点和终点,那么复杂度就不可能比O(n^2)低,那么我们能否只枚举终点或者起点呢?

  答案是可以。我们这里只枚举终点,对于终点 j ,我们的目标找到一个 i ,使得Bj-Bi-1 >= S,并且 i 尽量大,这里因为我们构造的数组B是递增的,所以这里可以二分!!

  这里提到了STL的应用! 这个还是很有必要掌握的,回头看看吧。  

  优化3:这里还有一个特点:我们在遍历j的时候,发现满足条件的 i 也是递增的! 这样的话我们也可以每次记录上次查找到的 i 的位置,最终只需要2遍遍历即可:

  一遍为 i ,一遍为 j 的遍历!!

心得:

  很多题目由于题目要求的约束,所以存在很多特殊的地方,我们需要做的就是耐心地一点点分析题目,认真对待每道ACM题目,因为它们每一道都是ACMer的心血,结晶,里面蕴含都有出题者特殊的含义。我们需要耐心,如高中做题目一样,发掘题目中的条件来解答,不要过于依赖于计算机,真正的提高,或者说比起别人来的优势,其实是在分析阶段。 

  而上了大学之后,我也发现自己看一道ACM题目,没有以前对待数学那种严谨、探究的态度了,这点一定要慢慢改,找回以前的感觉!加油!

 

 1.4动态规划:

11个小问题导引:

(1)数字三角形:(按步走)

问题描述:

  一个三角形的数塔,从上往下走,使得沿途数字和最大。

公式:

  按照阶段分类:设d(i,j)为从顶端出发到格子(i,j)所能得到的最大数字和。

  d(i,j) = max(d(i-1,j),d(i-1,j+1)) + a(i,j); 最终d(n,n)即为所求。

(2)嵌套矩形:需要先建模。(DAG上的最长路)

公式:

  设 d(i) 为以矩形 i 结尾的最长链长度。

  则: d(i) = max( 0,d(j));   ( j 可以嵌套在 i 中。)

(3)硬币问题:(DAG上的最长路,最短路问题)

公式:

  设 f(i) 和 g(i) 分别为面值之和恰好为 i 时,硬币数目的最小值和最大值。

  f(i) = min( ∞ ,f(i-V(j))+1 )  |  v(j)<=i             g(i) = max( -∞ , g(i-v(j))+1 )  | v(j)<=i

  边界条件: f(0)=g(0)=0;

(4)01背包问题:(选与不选)

公式:

  f(i,j) 表示 把前 i 个物品装入容量为 j 的背包中,得到的最大总重量。

  f(i,j) = max( f(i-1,j) , f(i-1,j-v(i))+wi )    vi <= j   

  边界为: f(0,j) = 0;

(5)点集配对问题:(内含重复子问题)

公式:

  d(S) 表示集合S配对后的最小距离和。

  d(S) = min(d(S-{i}-{j}) + P(i,j) )   其中 j 在S中, i 为S中的最小元素!

  书上61页有树状图。

(6)最长上升子序列问题:

公式:

  d(i) 表示以 i 结尾的最长上升子序列的长度;

  d(i) = max (0, d(j))+1  其中:   j

优化:

  d(j) 的比较中有很多无意义的比较,如果想要优化,则这里需要记录一些信息,来省去部分不必要的比较,以空间换时间。(事情都是有代价的。)

  (这里更好的一点是新设的数组递增,用到了二分。)

(7)最长公共子序列问题:

公式:

  d(i,j) 表示 A中前i个与B中前j个的最长公共子序列。

  if(A(i)==B(j))  d(i,j) = d(i-1,j-1)+1;

  else{

    d(i,j)=max(d(i-1,j),d(i,j-1));

  }

(8)最大连续和:

公式:

  d(i) = max(0,d(i-1))+A(i);    ----前一部分和大于0,则累加;否则舍去。(有点贪心的含义)

  最后再遍历一遍数组求得max。

(9)货郎担问题:(图)

公式:

  d(i,S) 表示当前在城市i,访问集合S中所有城市一遍之后返回0城市的最短路径。

  d(i,S)=min(d(j,S-{j}))+dis(i,j)     j 在集合S中遍历。    最终答案为: d(0,S)--S为全集。

(10)矩阵链乘:

公式:

  f(i,j)=max( f(i,k-1) , f(k+1,j)) + p(i-1,k,j);  ----k从i~j中枚举。

(11)最优排序二叉树问题:

公式:

  d(i,j) 表示从 i~j 建立的排序二叉树的最小检索次数。

  d(i,j) = max( d(i,k-1) + d(k+1,j) )+w(i,j);     |    k 从i~j之间枚举。

  这里使用max的含义:当前层取得最大值时,余下的取值在乘上层数后才会更小! 

 

例题:

例26:约瑟夫问题变形

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(第一章:算法设计基础(思维体操+常见策略+高效算法+动态规划))