zoukankan      html  css  js  c++  java
  • 清北学堂算法&&数据结构DAY1——知识整理

    简述:

      今天主要讲分治(主要是二分)、倍增、贪心、搜索,还乱入了爬山算法和模拟退火(汗。。。)

    一、分(er)治(fen):

      二分是个在OI中广泛运用的思想,随便举些例子,就足以发现二分的运用的广泛性:二分查找、二分答案;归并排序、快速排序;线段树、二叉查找树;0-1线性规划以及经常出现的搭配某个算法的二分题。至于分治,是解决一类可合并问题的法宝。

      对于一道满足二分性的题,我们就可以考虑用二分做它。二分性的实质是存在一个单调性或是临界点: 

        单调性:可能的答案在整体或在某一的区间是单调的,就可以对这个区间二分,找到这个区间的最优答案。

         

        临界点:答案的可行性在某一点突然发生突变,即把所有可能的答案画作一条线段的话,会明显发现以一个点为界限,左边的数据都可行,而右边都不可行。而那个界限一般就是我们要找的最优化问题的解。所以二分答案的实质也是用二分的方法找一个界限。

      

      二分通过每次处理都把要处理的有序区间缩小一半,达到了迅速的O(log n)的复杂度,并将最优化问题转化为判定性问题,再配合其他的一些算法,为很多最优化问题的解增添了一份可能。事实上,有些文字能让人意识到某道题可以二分(要对这些文字敏感,有些题说的没这么直接,要能看出来):

          最大值最小;

          最小值最大;

          有序;

          分数;

          时间复杂度;

          ……

    有时遇到一些看起来像是二分的题目,可以先打个表看一下是否满足二分性。

      一个简单的模板:

     

      (judge函数为判定当前枚举的界限mid能否可行。如果可行的话说明mid有可能就是最终的答案,同时也有可能有更优的答案,别忘记录一下)

    来看到简单的题:

    (入门题…)看到了“最大值最小”,要敏感哦。

      二分最小的最大值mid,接着用贪心扫一遍判定一下能否分成最大值小于mid的m(m<=M)段就行了。

      直接求可能有些难度,但看到“请你设计一种方案,使得复制时间最短。复制时间为抄写页数最多的人用去的时间”,这不就是“最大值最小”的另一种说法吗?,所以还是要对二分的关键词敏锐些!考虑二分答案,二分最小的最大值,从后往前扫一遍用贪心判定就行了。  

    二分的一般思路即为:确定要二分——要二分什么——判定什么——怎么判定(用什么算法)。

      一个最优化问题,可以考虑一下二分。显然要二分能组成的套牌数mid,判定能否用当下的牌组成这么多的套牌。记录每种牌到mid不足量的和sum。显然每种牌的不足都要靠joker来补足。首先发现每套牌最多只能有一个joker,所以sum应<=mid,否则判定结果为false;还发现joker最多只有m张,所以sum应<=m,否则判定结果为false。只要上面两个条件都满足,我们就可以用当下牌组成每套牌最多只有1个joker的mid套牌。

      放在U盘中的文件的最大l即为接口大小+“最小需要多大的接口”=最大值最小!考虑二分答案。二分最小的接口大小mid,把文件按大小从小到大排个序,将所有大小小于mid的物品做一个01背包,看看能否满足最大总价值不小于p即可。

      可先跑一遍广搜判断有无解以及从1到n的最少经过几个电话线杆以得知能否直接被电信公司报销全部费用。如不有解且不能报销全部费用的话,再更深地思考一下。又发现了“最大值最小”这一主题,再考虑二分。二分总费用mid(因超过k条而不被电信公司免费的最长的电话线长度),此时长度小于等于mid的电话线可以尽情搭,而大于mid的电话线则全让电信公司给免费(若小于k条的话),故可以把所有边权<=mid的边的边权都修改为0,大于mid的都修改为1,跑一遍最短路(边权只有01的最短路可用双端队列的广搜实现)。若得到的最短路径长度len<k,则说明电信公司还可再多给免费几条电话线,mid还有可能更小,记录答案后在r=mid-1;若len==k,说明刚好把需要的能免费的电话线都免费了,输出mid即可;若len>k,说明“免费超额”了,要把mid改大点,即l=mid+1。

     

    讲点有趣的东西吧。如何生成一个最优比率生成树?

      

    二分的一个十分优雅又不失尴尬的考法(。。。),这也是为什么碰到分数可以考虑二分的原因。以最大为例,设最终答案为ans,则所有大于ans的可能答案都不会有一个生成树满足条件,而小于ans的可能答案总会被一个生成树的答案大于,发现ans即为一个界限,可以用二分。这里二分答案k,即要判定是否存在一个生成树使∑(benifit[i])/∑(cost[i])>=k,让k尽可能大、尽可能去接近最终答案就好了。

          由于具有重大的现实意义,不妨假设cost都大于0。

          则∑(benifit[i])>=k*∑(cost[i]);

          再变一下:k*∑(cost[i])-∑(benifit[i])<=0;

            ∑(k*cost[i])-∑(benifit[i])<=0;

            ∑(k*cost[i]-benifit[i])<=0;

      看到这里是不是就懂了?只要我们再把所有边的边权变为k*cost[i]-benifit[i],再跑一遍最小生成树,把权值之和与0比较,若<=0则判定为true,否则为false。

     以后对于类似的分数最优化,都可以用类似的变形随便变变,尝试用二分做。

      平均乐趣值最大,实际上就相当于总收入/总花费最大。

      小技巧:对于一个既有边权又有点权的有向图,我们可以把点权挪到边权上去。因为我们一旦踏上某条边,这条边的终点我们一定也会经过。而对于这道题来说,尽管每个点的点权只能被加一次,可是考虑一下八字形的情况(即有点重复经过多次的代表),这种情况实质上是由多个环组成的情况。由于题目要求的分数是一种平均数,易知组成那个八字形的两个环中一定有一个环的   一定。故这题又是分数规划,思路跟楼上非常像,不过最优比率生成树变成了最优比率环而已。而对于判是否有环的权值和为负(为0的情况不管也没什么啦),这不就是判负权回路吗?bellman-ford与spfa任君选择。。。

      关于bellman-ford以及进阶的spfa,您可以看看作者的另一篇博客:Bellman-ford算法与SPFA算法思想详解及判负权环(负权回路)

    二、倍增:

      跟二进制有着密切的不可告人的联系,看见二进制及2的k次方,想想倍增准没错!

      常用于快速幂、快速乘(明明一点都不快)、快速矩阵乘法(主要还是矩阵快速幂)、倍增求...(LCA出现居多)。

      1、快速幂:我们算a的b次方模p的值。一般是O(n)算法乘一波,当b特别大时显然不行。考虑将B二进制分解,就可O(log n)算出结果。

      2、快速乘:

      

      这就没了??(心里一句***)

      果然关键时刻还得靠大佬:O(1)快速乘 - 紫芝的博客 - CSDN博客

      3、矩阵乘法:用于求一类常系数递推方程,这也是矩阵乘法的一个主要用途了。简单的说,对于一个一次的常系数递推方程(如f(n)=7*f(n-1)+2*f(n-2)+5)(目前只会这个QAQ)计算f(n),一般算法都是O(n)的递推,但当n特别大时肿么办?

      我们竖着写一个m*1的矩阵a,第i行分别为方程中去掉系数的第i项,如果为常数则写为1(比如这里的an-1的三项从上往下分别为f(n-1),f(n-2),1)。都知道矩阵乘法是一个n*k的矩阵乘一个k*m的矩阵得到一个n*m的矩阵,而矩阵乘法不满足交换律,但满足结合律和左右分配率律。只要我们在矩阵a左边写一个矩阵j(称为转换矩阵),要求j*a能得到下一个a(即f(n),f(n-1),1),从最开始的a0开始,每被j乘一次ai就变成ai+1,由矩阵的结合律可知,只要算出j的n次方(用快速幂,若要取模,直接对矩阵每一项取模就行)后再与a0相乘得到an,答案就为an的第一项。时间复杂度从O(n)进化为O(log n)。

      4、倍增求LCA:快速地(O(log n))求树上的最近公共祖先,以迅速处理树上的路径问题(dis(u,v)=dis(u,root)+dis(v,root)-2*dis(lca(u,v))。

    看题喽!:

      转换矩阵为:

          1 1

          1 0

     

      如果对于每一支军队都做一遍所有指令后再看下一个军队,显然会超时。为什么?是不是我们看待军队的角度不对?如果我们把军队i写作一个矩阵:

      

    xi
    yi
    1(有常数参与矩阵乘法时常常需要个1)

      同时对于三种操作,也可以写出相应的矩阵:  

      操作1:

    1 0 p
    0 1 q
    0 0 1

      操作2:

    -1 0 0
    0 1 0
    0 0 1

      操作3:

    1 0 0
    0 -1 0
    0 0 1

      因为所有军队收到的命令相同,又有矩阵乘法的结合性,故可将所有操作矩阵乘起来得到一个结果矩阵,在用这个结果矩阵分别去乘每一个军队对应的矩阵就得到答案了。时间复杂度O(n+m)。

    矩阵的另一个大用途就是把一堆让人头疼的东西抽象化。只要抽象化成一个数学结构,问题一般就好解了。

      显然可以用递推方程做,但发现方程不好写。为什么不好写?主要还是因为对不同范围的数,对应的方程和转换矩阵也不太一样。先不要放弃,分段考虑尝试一下,惊奇地发现转移矩阵竟可以用一种方法表示出来:

      

     

    只要看到异或,我们就应该想到一个数异或自己等于0(凭此据说可以用三次异或来交换整形变量),一个数异或0仍等于它自己,且异或满足交换律、结合律与分配律。类比求树上两点间的路径长度dis(u,v)=dis(u,root)+dis(v,root)-2*dis(lca(u,v),不过在这里设dis(u,v)为两点间路径上所有边权的异或值。发现公式改成dis(u,v)=dis(u,root)+dis(v,root)就行了!因为dis(u,root)+dis(v,root)相较于dis(u,v)只多了2个dis(lca,root),然而dis(lca,root)异或下自己就等于0了,所以最终结果仍是dis(u,v)。所以只要处理出每个点到根root的路径上所有边权的异或值就行了。

      容易知道选的点一定是三个点的两两LCA的其中一个。直接求三遍lca到三点的距离,去最小值就好了。

    三、贪心

       

      贪心策略的证明:枚举所有情况,都不会比它更优了。

      

      非常简单的贪心:先合小的。搞一个小根堆就好了。

      //(dms正解:维护哈夫曼树???)

        插入哈夫曼树的有关知识:

          

        (原博客:https://www.cnblogs.com/dalt/p/8001560.html  代码为不严格的伪代码,只求明义。满二叉树的定义遵循国外(国际)定义)

        我们画一棵每个非叶节点都有两个子节点的二叉树,以叶节点表示起始所有的石子,每两个兄弟节点连向同一个父亲节点即视为一次合并,发现此二叉树在哈夫曼树定义下的权值即为答案,故要答案最小的话,维护一颗哈夫曼树就好了。

      由于出现的字母固定,首先想到了字典树,两两互不为前缀,即要求用字典树的每个叶节点代表单词,可在所有叶节点记录下某个单词出现的次数,那么这个字典树在哈夫曼树定义下的权重就是整个文章的长度了。由此想到了哈夫曼树,不过在这里是K叉的特殊情况。

      看看K叉哈夫曼树对于二叉哈夫曼树在上面的命题中有什么变化:

        对于命题1,显然这个K叉哈夫曼树不一定是满K叉树了,不过能证得所有非叶节点最少有2个儿子(虽然没什么用)。

        命题2仍然成立。

        命题3则强化为在满k叉树的情况下最小的k个节点连向同一个父亲f(满k叉树的情况下,若深度浅的地方有点属于前k小的点(不考虑相等的情况,若相等的话可随便,不会影响结果),必有一点x比他大且为f的儿子,把它与x交换,能得到权值更小的树)。

      由构造二叉哈夫曼树的方法联想到构造k叉哈夫曼树的方法:可以每次都将当前k个最小的拿出来、连到同一个父节点上再把那个新建的父节点放回堆里。但这样做会有一个明显的错误:如果最后一次从堆取出的节点少于k个,就会导致根结点的儿子数少于k个。这样的话就可以从孙子一辈(如果有孙子的话)随便拿来个点连根上,使整棵树的权值减小,故不是合法的哈夫曼树。

      按上文“错误”的方法来看,每次我们都从堆里取出k个点,放回1个点,相当于取出(k-1)个点,最后要剩下一个点作为哈夫曼树的根。若最后一次取出的节点正好为k,则整棵哈夫曼树为满k叉树,不会出现上文的那个明显的错误。这时我们从堆里取出了很多次(k-1)个点,堆里还剩1个点,一共有n个点,故(n-1)mod (k-1)=0 ,即(n-1)为(k-1)的倍数。那么当(n-1)为(k-1)的倍数,即构造的哈夫曼树为满K叉树时可以吗?

      可以按照证二叉情况的相似思路证明:

        通过归纳法说明,当只有一个顶点或只有k个及以内的节点时,算法显然正确。当顶点数少于m时,若上述算法都可以构建一株合理的哈夫曼树。那么当我们持有m个结点组成的结点集合V时,由于命题三知权重最小的k个结点组成的节点集合A可以有相同的父亲f,我们利用上述方法,使用m-(k-1)个结点组成的结点集合V'(V中移除了A 后加入f得到)建立对应的一株哈夫曼树F。假设T为V的哈夫曼树。我们可以在T的基础上建立一株新树T',其中T'与T的区别在于我们为f赋予权值A.w,同时从T中删除A,显然T'.w = T.w -A.w。而T'也是满足以V'为叶结点的一株二叉树,故知F.w<=T'.w=T.w-A.w。同样我们可以在F的基础上建立另外一株二叉树F',其中F'与F的区别在于我们移除f结点的权值,并为其添加A,此时显然有F'.w=F.w+A.w,而由于F'.w是V的一株二叉树,因此T.w<=F'.w=F.w+A.w。结合两条不等式可以得出F.w+A.w=T.w,即在F的基础上做改变得到的树F'是V的哈夫曼树。因此我们可以通过递归的思路建立哈夫曼树。

      对于哈夫曼树不为满k叉树的情况怎么办呢?我们可以加一些“零点”(即点权为0的点),这样并不会影响到整棵树的权值。在n个点的基础上加上几个零点得到n'个点使(n'-1)为(k-1)的倍数,这样又可以转化为满k叉树的情况做了。

      最后在考虑怎样让最长的si最短。显然能看出由于儿子顺序、相同值节点顺序的不确定性,哈夫曼树不是唯一的,这导致可能会在最后一问栽跟头。其实可以用贪心解决,对于权值相同的节点,深度小的优先选择,这样就能保证最后最长的si最短了。

     

      若图中两点间存在一条各边边权都小于m的路径,则在最大生成树上两点间的唯一路径也符合各边边权小于m。可以这样理解:若图的最大生成树上有一条边的边权小于m,删去这条边会将树分割成2个连通块A和B,由于该边是连通块A到连通块B的最大边(参见这里的判断),故连通块A的所有点到连通块B的所有点的所有路径必有一条边边权小于m,即最大生成树上两点间没有合法路径,在整个图上这两点间也不会有合法路径。若图上有合法路径,则最大生成树上一定有。

      对于两点间路径中最小边权的最大值则可用倍增实现。对于有列车站的点,我们可以将它们用非常大的边(近似无穷大)连成一个连通块(这里用了最简单的环)(相当于不受重量限制的一种表现形式),最后模拟即可。

    上个AC代码吧:

      

      1 #include<iostream>
      2 #include<cstdio>
      3 #include<cstring>
      4 #include<queue>
      5 #include<cmath>
      6 
      7 using namespace std;
      8 
      9 const int MAXN=100000,MAXDIS=1000000000;
     10 
     11 long long ans,ord[MAXN+5];//ord存单子 
     12 
     13 long long lim[MAXN+5];//每个城市的交易限制 
     14 
     15 char ch;
     16 
     17 bool fu;
     18 
     19 inline long long getint()//这里应该是getlonglong,后来懒得改了 
     20 {
     21     ans=0;
     22     ch=getchar();
     23     fu=0;
     24     while(!isdigit(ch)) fu|=(ch=='-'),ch=getchar();
     25     while(isdigit(ch)) ans=(ans<<3)+(ans<<1)+(ch^48),ch=getchar();
     26     return fu?-ans:ans;
     27 }
     28 
     29 int n,m,Q,lst[MAXN+5],to[MAXN<<3],nxt[MAXN<<3],cnt;//边的限制 
     30 
     31 long long dis[MAXN<<3];
     32 
     33 long long db[18][MAXN+5],leth[MAXN+5]; 
     34 
     35 inline void addedge(int u,int v,long long w)
     36 {
     37     nxt[++cnt]=lst[u];
     38     lst[u]=cnt;
     39     to[cnt]=v;
     40     dis[cnt]=w;
     41 }
     42 
     43 bool vis[MAXN+5];
     44 
     45 struct node{
     46     int hao;
     47     long long len;
     48 }head;
     49 
     50 inline bool operator < (const node &a,const node &b)
     51 {
     52     return a.len<b.len;
     53 }
     54 
     55 priority_queue<node> q;
     56 
     57 int fa[18][MAXN+5],dep[MAXN+5];
     58 
     59 #define min(a,b) ((a)<(b)?(a):(b))
     60 
     61 inline int Log2(int a)
     62 {
     63     return log(a)/log(2);
     64 }
     65 
     66 inline void Prim()//最大生成树(要建树) 
     67 {
     68     memset(leth,128,sizeof leth);
     69     leth[1]=0;
     70     int ok=0,t,too,f,lo,d;
     71     q.push((node){1,0});
     72     fa[0][1]=0;
     73     dep[0]=-1;
     74     while(ok<n)
     75     {
     76         head=q.top();
     77         q.pop();
     78         if(vis[t=head.hao]) continue;
     79         vis[t]=1;
     80         f=fa[0][t];
     81         d=dep[t]=dep[f]+1;
     82         if(t!=1)
     83         {
     84             lo=Log2(d);
     85             for(int i=1;i<=lo;++i)
     86             {
     87                 fa[i][t]=fa[i-1][fa[i-1][t]];
     88                 db[i][t]=min(db[i-1][t],db[i-1][fa[i-1][t]]);
     89             }
     90         }
     91         ++ok;
     92         for(int e=lst[t];e;e=nxt[e])
     93         {
     94             too=to[e];
     95             if(vis[too]==0&&leth[too]<dis[e])
     96             {
     97                 leth[too]=dis[e];
     98                 fa[0][too]=t;
     99                 db[0][too]=dis[e]; 
    100                 q.push((node){too,dis[e]});
    101             }
    102         }
    103     }
    104 }
    105 
    106 #define max(a,b) ((a)>(b)?(a):(b))
    107 #define swap(a,b) ((a)^=(b),(b)^=(a),(a)^=(b))
    108 
    109 inline long long lca(int x,int y)//倍增求最大的最小边权 
    110 {
    111     if(x==y) return 0x7ffffffff;
    112     if(dep[x]<dep[y]) swap(x,y);
    113     int lo;
    114     ans=0x7ffffffff;
    115     if(dep[x]>dep[y])
    116     {
    117         lo=Log2(dep[x]-dep[y]);
    118         for(int i=lo;i>=0;i--)
    119             if(dep[fa[i][x]]>=dep[y])
    120             {
    121                 ans=min(ans,db[i][x]);
    122                 x=fa[i][x];
    123             }
    124     }
    125     if(x==y) return ans;
    126     lo=Log2(dep[x]);
    127     for(int i=lo;i>=0;i--)
    128         if(fa[i][x]!=fa[i][y])
    129         {
    130             ans=min(ans,db[i][x]);
    131             x=fa[i][x];
    132             ans=min(ans,db[i][y]);
    133             y=fa[i][y];
    134         }
    135     ans=min(ans,db[0][x]);
    136     ans=min(ans,db[0][y]);
    137     return ans;
    138 }
    139 
    140 inline void MONI() 
    141 {
    142     int now,thenxt;
    143     long long ag,xian;
    144     if(!n) return;
    145     now=ord[1];
    146     ag=max(0,lim[now]);
    147     if(lim[now]<0)
    148     {
    149         putchar('0');putchar('
    ');
    150     }
    151     for(int i=2;i<=n;++i)
    152     {
    153         thenxt=ord[i];
    154         xian=lca(now,thenxt);
    155         ag=min(ag,xian);
    156         if(lim[thenxt]>0)
    157             ag=ag+lim[thenxt];
    158         else
    159         {
    160             xian=min(ag,-lim[thenxt]);
    161             ag-=xian;
    162             printf("%lld
    ",xian);
    163         }
    164         now=thenxt;
    165     }
    166 }
    167 
    168 int main()
    169 {
    170     n=getint(),m=getint(),Q=getint();
    171     for(int i=1;i<=n;++i) ord[i]=getint();
    172     for(int i=1;i<=n;++i) lim[i]=getint(); 
    173     int u,v;
    174     long long w;
    175     for(int i=1;i<=m;i++)
    176     {
    177         u=getint(),v=getint(),w=getint();
    178         addedge(u,v,w);
    179         addedge(v,u,w);
    180     } 
    181     int fir=0,now=0;
    182     if(Q)
    183         fir=now=getint();
    184     for(int i=2;i<=Q;++i)
    185     {
    186         v=getint();
    187         addedge(now,v,0x7ffffffff);
    188         addedge(v,now,0x7ffffffff);
    189         now=v;
    190     }
    191     if(now!=fir) //不要忘了要把链接成环 
    192     {
    193         addedge(now,fir,0x7ffffffff);
    194         addedge(fir,now,0x7ffffffff);
    195     }
    196     Prim();
    197     MONI();
    198     return 0;
    199 } 
    AC代码(巨长慎点)

    四、搜索

       1、基础——枚举:将所有可能需要的情况列出来求解题的算法。一般复杂度都很高(除了一些巧妙/高级的枚举)。

      例子:用不断求下个排列的函数next_pernutation生成全排列

         枚举子集

    EX.1、爬山算法

    EX.2、模拟退火

     

    继续看搜索

  • 相关阅读:
    e生保plus
    Exception analysis
    经验总结:5个应该避免的前端糟糕实践
    经验总结:应对中文输入法的字符串截断方案(带代码示例)
    这些年那些文
    fis入门-单文件编译之文件优化(optimize)
    《HTTP权威指南》读书笔记:缓存
    npm install —— 从一个简单例子,看本地安装与全局安装的区别
    chrome下的Grunt插件断点调试——基于node-inspector
    Yeoman的好基友:Grunt
  • 原文地址:https://www.cnblogs.com/InductiveSorting-QYF/p/11240846.html
Copyright © 2011-2022 走看看