前言
图论是算法中的一个重要内容,图的范围很广,包括二叉树、树、图等。今天我们来学习树的重要内容——并查集。

程序设计思路
并查集是一个森林(由一棵或多棵树组成的集)。我们可以利用其特性进行一些操作。
在并查集中,最重要的操作便是并和查。
并是指将两棵树合并为一棵。这很好进行,因为并查集里的每个数都有一个指针指向自己的父亲。
查找也不难,只需要一个递归程序,不断查找父亲的父亲,最后找到最老的祖先再返回(注意:并查集中最老的祖先的祖先是其自身)。
比较重要的是一个优化方案——路径压缩。由于查找是递归调用,故当要找的祖先过于遥远时,我们可以使用路径压缩,将一棵树中的全部非根节点的祖先直接设置为最老的祖先。路径压缩通常在执行查找命令时进行。

代码实现
下面通过并查集的模板题讲解其用法:
202408231724417005184545.png
这题要求我们模拟并查集的操作,直接上代码:

#include <bits/stdc++.h>
using namespace std;

int n,m,z[200005],x[200005],y[200005],bin[10005];//z、x、y如题意,bin为并查集中每个数的祖先(父亲)

int find(int a){//查找操作
    int x=a;
    if(bin[x]!=x) return bin[x]=find(bin[x]);//在查找的过程中路径压缩
    else return x;//当查到某个数的祖先是它自己时,说明找到了祖先,开始返回
}
void join(int b,int c){//并操作
    if(find(b)!=find(c)) bin[find(b)]=find(c);
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) bin[i]=i;
    for(int i=1;i<=m;i++) scanf("%d%d%d",&z[i],&x[i],&y[i]);
    for(int i=1;i<=m;i++){
        if(z[i]==1) join(x[i],y[i]);
        else{
            if(find(x[i])==find(y[i])) printf("Y\n");
            else printf("N\n");
        }
    }
    return 0;
}

这样,我们就较好的模拟了一个并查集。

结语
并查集是图论中较为简单的一科,只需要记下并、查两个操作就可以解决相关问题。我是faryou,再见!

前言
上一篇文章中我们学习了简单DP,今天我们来学习一种也很常见的DP——背包DP。

程序设计思路
背包DP以01背包和完全背包为基础,其他的背包DP都由这两种延伸出去,先看01背包:
01背包是最简单的背包DP,它是指有n样物品,第i样物品的价值为xi,所占用的容量为yi,你有一个容量为m的背包,要求你用这个背包装下价值最高的物品,每种物品只能装一次。这里每样物品的状态都只有装或不装。故我们可以列出状态转移方程:fi=max(fi-1,fi-1+xi(fi表示在第1到第i个物品中选容量为j的物品能获得的最多的价值)。
完全背包与01背包的区别在于它每样物品可以选无数次,但它的状态转移方程和01背包有不同。我们可以列出状态转移方程:fi=max(fi-1+k*xi)(这里的k是指购买了k件i物品,是for循环中的变量),这样做的时间复杂度为O(n3),明显偏慢了。但是我们还有下面的优化:fi=max(fi-1,fi+xi),因为我们在进行这次转移前就已经充分的考虑了。这个优化必须自己想清楚。

代码实现
来看一个01背包问题:
202408231724413377911411.png
很明显,这是一道背包题。直接看代码:

#include <bits/stdc++.h>
int n,m,a[105],dp[105][1005];//n、m、a如题意,dp指上文中i

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){//分类讨论,判断能不能买
        if(j==a[i]) dp[i][j]=dp[i-1][j]+1;
        if(j>a[i]) dp[i][j]=dp[i-1][j]+dp[i-1][j-a[i]];
        if(j<a[i]) dp[i][j]=dp[i-1][j];
    }
    printf("%d",dp[n][m]);
    return 0;
}

DP注重的是自己的理解,而且讲解无法达到很好的效果,请自行理解源码~

结语
背包问题是DP中较为简单的一类,要想学好DP,就应该多刷题,以此总结经验。我是faryou,再见!

前言
上一篇文章中介绍了递推,其实也是为今天的DP打好基础。

程序设计思路
DP本质上就是对题目进行分类讨论,列出其不同情况下的状态转移方程,然后套上循环求解。
DP的种类有很多,其应用范围几乎涵盖了全部的算法内容,理论上来说,只要你有能力列出状态转移方程,DP可以解决几乎一切问题。今天我们讲一下简单的DP,了解一下DP的思路。

代码实现
由于DP的可拓展性太强,今天我破例用两道题进行讲解:
202408221724319442758119.png
这道题要求我们得到最大的和。这里有一个思路:每次将金字塔底部相邻的两个数相比较,将大者加到这两个数正上方的数上。即:fx=max(fx,fx+1)+fx(金字塔存储为直角三角形)。下面是代码:

#include <bits/stdc++.h>
int r,f[1005][1005];//r如题意,f为DP用数组
int main(){
    scanf("%d",&r);
    for(int i=0;i<r;i++) for(int j=0;j<i+1;j++) scanf("%d",&f[i][j]);//读入数据
    for(int i=r-1;i>0;i--) for(int j=0;j<r-1;j++) f[i-1][j]+=max(f[i][j],f[i][j+1]);//递推式
    printf("%d",f[0][0]);//输出塔尖
    return 0;
}

这道题是经典动规题,我们通过层层上推求出结果。再来看下面这题:
202408221724328272181898.png
本题初看没有思路,但是细细一想,一个长度为n的上升子序列可以分为前面长度为n-1的上升子序列和后面的一个数,那么我们可以先把所有f[x]都初始化为1,之后用双层循环(双指针),将后面的大数和前面的子序列拼为一个序列。由此,我们可以列出这样的方程:f[x]=max(f[i],f[j-1])(f[x]表示从第1到第x个数的最长上升子序列长度),下面是代码:

#include <bits/stdc++.h>
int n,ans=0,a[5005]={0},f[5005]={0};
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        f[i]=1;
    }//以上为读入+初始化(一个数自己也算最长上升子序列)
    for(int i=1;i<=n;i++) for(int j=1;j<=i-1;j++) if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);//递推式,当找到一个更大的数时,将其加入前面的子序列
    for(int i=1;i<=n;i++) ans=max(ans,f[i]);//拿到最长长度
    printf("%d",ans);//输出
    return 0;
}

结语
其实DP本身的思路不难,关键在于你能不能找出所有情况。下篇文章,我将介绍背包DP。我是faryou,再见!

前言
在算法中,递推是一种重要的方法。通过列出递推式,我们可以解决许多棘手的问题。

“状态”
在算法中,状态是指某个变量在某种特定情况下的值。例如当i>=0时,abs(i)==i;而当i<=0时,abs(i)==-i。如果我们把这里的abs(i)赋值给一个变量j,那么这个变量j的状态(值)就随着i的变化而变化。
就上面这个例子,我们可以列出关于j的状态转移方程:当i>=0时,j[i]=i;当i<=j时,j[i]=-i。

程序设计思路
在解决具有嵌套性质的问题时,尤其是当n的值不同,且各个答案之间有关联的问题时,列出一个正确的递推式,就几乎等于解决了问题,不必进行暴力搜索等操作。
由于直接讲解比较抽象,所以我们直接来看题目。

代码实现
202408221724303365825395.png
可以看到,这道题要求我们计算走楼梯的步数。很显然,如果我们使用搜索来解题,那么速度将会偏慢,导致TLE。所以,这里我们使用递推来解决问题。
由题意,可知走到第x个台阶,一定是从第i-1个台阶或从第i-2个台阶出发。由此,我们可以列出方程:f[x]=f[x-1]+f[x-2](f[x]表示走到第x个台阶的方案总数)。
列出方程后,我们只需要找到这个方程的初始状态就可以求解了。而不难看出f[1]=1,f[2]=2。问题解决!
你以为这就完了?No!注意数据没有范围,要用高精度!!!(如果不知道怎么用高精度的可参考:https://blog.faryou.eu.org/tech/index.php/archives/tech/29.html
代码如下:

#include <bits/stdc++.h>
using namespace std;

int a[5010][5000],len=1,n;//a相当于上文中的f(高精度所以用二维),len存储位数,n见题意

void add(int arr){
    for(int i=1;i<=len;i++) a[arr][i]=a[arr-1][i]+a[arr-2][i];//高精度一位一位加
    for(int i=1;i<=len;i++) if(a[arr][i]>=10){//进位
        a[arr][i+1]+=a[arr][i]/10;
        a[arr][i]%=10;
    }
    if(a[arr][len+1]>0) len++;//位数++
    return ;
}
int main(){
    scanf("%d",&n);
    a[0][1]=1;//初始值,因为f[2]=2=f[1]+1
    a[1][1]=1;
    for(int i=2;i<=n;i++) add(i);//重复计算
    for(int i=len;i>0;i--) printf("%d",a[n][i]);
    return 0;
}

结语
递推是一个极为重要的方法,后面要学习的DP(动态规划)本质上就是分类讨论列出不同情况下的状态转移方程。我是faryou,下次见!

前言
前面我们已经学习了二分算法。其实,对于一些特定问题,我们还可以使用三分算法。

程序设计思路
三分,顾名思义,就是把数据分成三段,使用四个指针,其基本方向仍是缩小范围。我们可以重复检查左中指针与右中指针,判断答案在哪一侧,将左指针或右指针移动到中指针,再重新计算左右中指针,实现缩小范围。
三分算法主要适用于曲线形函数的求解,因为这类题目一般都是求其在峰上的值。

代码实现
直接看题目:
202408211724235830591510.png
题目要求很清楚:求函数值为峰值时,x的值,看代码——

#include <bits/stdc++.h>
using namespace std;

double n,l,ml,mr,r,iq[13];//l、ml、mr、r分别为左、左中、右中、右指针。iq数组存放系数

double f(double x){//即check,验证函数
    double ans=0;
    for(int i=0;i<=n;i++) ans+=iq[i]*pow(x,n-i);
    return ans;
}

void sett(){//重算左中、右中指针
    ml=l+(r-l)/3;
    mr=l+((r-l)/3)*2;
    return ;
}

int main(){//输入输出,不解释
    scanf("%lf%lf%lf",&n,&l,&r);
    for(int i=0;i<=n;i++) scanf("%lf",&iq[i]);
    while(fabs(l-r)>1e-5){
        sett();
        if(f(ml)>f(mr)) r=mr;
        else l=ml;
    }
    printf("%.5lf",ml);
    return 0;
}

由此可见,三分实质上就是二分加上了一个指针,从而使答案限制在峰顶,最后求出结果。

结语
相比于二分,三分的应用场景小了很多。不过对三分的学习,其实也可以当成对二分的复习。我是faryou,再见!