清北学堂算法&&数据结构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:

10p
01q
001

 

  操作2:

-100
010
001

  操作3:

100
0-10
001

  因为所有军队收到的命令相同,又有矩阵乘法的结合性,故可将所有操作矩阵乘起来得到一个结果矩阵,在用这个结果矩阵分别去乘每一个军队对应的矩阵就得到答案了。时间复杂度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('\n');
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\n",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、模拟退火

 

 

继续看搜索

 

转载于:https://www.cnblogs.com/InductiveSorting-QYF/p/11240846.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值