NOIp2016算法



NOIp2016算法

程序大部分没经过编译和测评,欢迎在评论区指出本文中的错误
另:点开右下角像WIFI一样的标志进入RSS模式即可避免索引目录鬼畜的问题(可能需要向下翻一会才能找到本篇文章)

NOIp竞赛中还可能会用到的模板:Tarjan等,请大家注意一下

记得考前一定要练好DFS和记忆化DFS!!骗分必用!

C++


Catalogue:

 

 


数据结构

int strack[maxn];
int head;
bool b[maxn];

void push(int x)
{
    strack[++head]=x;
    b[x]=true;
};

int pop()
{
    int ret;
    ret=strack[head--];
    b[ret]=false;
    return ret;
};

bool empty()
{
    return head>0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

队列

int queue[2*maxn];
int tail,head;
bool b[maxn];

void push(int x)
{
    queue[++tail]=x;
    bool[x]=true;
};

int pop()
{
    int ret;
    ret=queue[++head];
    b[ret]=false;
    return ret;
};

bool empty()
{
    return head>=tail;
};  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

当然有的时候你手写的数据结构需要比较大的空间,这样队列就会造成很多损失,所以相应的就有两种解决方法:一:STL;二:循环队列,只需改两个地方(代码如下);

head=(head+1)%n+1;//把head++改
tail=(tail+1)%n+1;//把tail++改
  • 1
  • 2

树状数组

int lowbit(int x)
{
    return x&-x;
};

int getsum(int n) //求1~n见的和
{
    int ret=0;
    while(n)
    {
        ret+=c[n];
        n-=lowbit(n);
    };
    return ret;
};

void add(int n,int x) //给a[n]加上x
{
    a[n]+=x;
    while(n<=maxn)
    {
        c[n]+=x;
        n+=lowbit(n);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

应用模型:

  • 树状数组求逆序对:
void update(int n)
{
    while(n<=maxn)
    {
        c[n]+=1;
        n+=lowbit(n);
    };
};

for(int i = 1; i <= n; ++i)  //主程序里面加上这个
{  
    update(reflect[i]);  
    ans += i - getsum(reflect[i]);//reflect是离散化后的数组
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

单调队列

以下单调队列的标程就用的音乐会的等待的。

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=500005;
int n,l,ans;
long long q[maxn],bef[maxn];
int main()
{
    cin>>n;
    q[0]=-1;
    long long a;
    for(int i=1;i<=n;i++)
    {
        cin>>a;
        while(l>0&&a>q[l])
        {
            l--;
            ans++;
        };
        if(l!=0)
        {
            if(a!=q[l])ans++;else
            {
                ans+=bef[l];
                if(bef[l]<l)ans++;
            };
        };
        l++;q[l]=a;
        if(q[l-1]==a)bef[l]=bef[l-1]+1;else bef[l]=1;   
    };
    cout<<ans;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

STL

关于每个STL我只会写一下是什么,怎么用(举例子的形式),不会说的太细

Vector

不定长度数组

#include <vector>
vector<int> first;  //第一种定义方法

int myints[]={16,2,77,29};
vector<int> second(myints,myints+4);//第二种定义方法

sort(second.begin(),second.end());//对vector排序

a=second[i];//可以这么使用

//以下是对vector的操作

Vector<int> opt;

opt.begin();    //返回起始地址
opt.end();  //返回结束地址
opt.size(); //返回大小
opt.empty();    //返回是否vector为空
opt.back(); //返回最后一个push进的数
opt.pop_back(); //把最后一个数弹出(不返回)
opt.push_back(int x);//把x从后面push进去

opt.erase(opt.begin(),opt.begin()+3);//删掉前三个元素
opt.erase(opt.begin()+5);//删掉第6个元素
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

Queue

队列,操作与Stack一样。

Priority_queue

相当于堆

#include <queue>

priority_queue<int> Bigheap;//定义一个大根堆
priority_queue<int,vector<int>,greater<int> > Smallheap;//定义一个小根对(注意两个尖括号间有空格)

//以下是操作

priority_queue<int> opt;
opt.top();//返回堆顶元素的值(不弹出)
opt.pop();//弹出堆顶元素(无返回值)
opt.push(x);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Stack

stack<int> opt;
opt.front();//返回
opt.size();
opt.empty();
opt.push();
opt.pop();//弹出
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Deque

双向队,操作与Stack一样

Bitset

压位神器,只普及一下,不会用。

Set

set<int> first;
int myints[]= {10,20,30,40,50};
set<int> second (myints,myints+5);
set<int> third (second);
set<int> fourth (second.begin(), second.end());
third.rbegin(); third.rend();//rend相当于begin,rbegin相当于end
third.size();//返回大小
third.insert(60);
third.erase(it);
third.erase(50);//删除元素'50'
third.find(10);//找元素'10'
third.lower_bound(30); third.upper_bound(30);//'30'出现的第一个位置/最后一个位置
third.clear();//清除
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

Multiset

与Set用法一样,只是允许重复元素。

Map

map<char,int> first;
first[‘a’] = 10;
first.insert(make_pair(‘b’,20));
it++; ++it; it--; --it;
first.erase(1);//删除元素
firstt.count(1);//看有没有关系
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Algorithm里其他好用的函数

Next_permutation

int a[]={1,2,3,4};

next_permutation(a,a+3);//下一个全排列

//现在a数组变成了:1 2 4 3
  • 1
  • 2
  • 3
  • 4
  • 5

Lower_bound与Upper_bound

lower_bound(first,last,val);//有返回值
upper_bound(first,last,val);
  • 1
  • 2

Merge

merge (first,first+5,second,second+5,v.begin(),compare);
  • 1

sort

bool compare(int a,int b)
{
    return a<b;
};//compare函数的例子

sort(起始地址,结束地址,compare函数);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Reverse

Reverse(myvector.begin(),myvector.end());
  • 1

Unique

bool myfunction (int i, int j)
{
  return (i==j);
}

unique(起始地址,结束地址,去重条件函数);//按照函数里面编写的规则去重,当然也可以没有第三个参数
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Random_shuffle

留一个概念,不会用,生成数据的时候用。

数论

快速幂

普通快速幂

#define ull unsigned long long
ull qpow(ull x,ull y,ull p) // x^y mod p
{
    ull ret=1;
    while(y)
    {
        if(y&1)ret = ret * x % p;
        x=x*x%p;
        y>>=1;
    };
    return ret;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

矩阵快速幂

matrix operator *(matrix m1,matrix m2)//重载运算符
{
    assert(m1.m==m2.n);
    matrix res;
    res.n=m1.n; res.m=m2.m;
    for(int i=0;i<m1.n;i++)
        for(int j=0;j<m2.m;j++)
            for(int k=0;k<m1.m;k++)
                res.mat[i][j]+=m1.mat[i][k]*m2.mat[k][j];
    return res;
}

matrix matrix_pow(matrix x,int y)
{
    matrix res;
    res.n=res.m=2;
    res.mat[0][0]=res.mat[1][1]=1;
    while(y)
    {
        if(y&1)res=res*x;
        x=x*X;
        y>>=1;
    }
    return res;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

筛法求素数

欧拉筛法

int tot=0;
void euler(int n)
{
    memset(check,0,sizeof(check));
    for(int i=1;i<=n;i++)
    {
        if(!check[i])prime[++tot]=i;
        for(int k=1;k<=tot;k++)
        {
            if(i*prime[k]>n)break;
            check[i*prime[k]]=1;
            if(i%prime[k]==0)break;
        };
    };
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

应用举例:

验证素数

普通方法

bool flag=true;
for(int i=1;i<=trunc(sqrt(prime));i++)
    if(prime%i==0)flag=false;//置不是素数标志
  • 1
  • 2
  • 3

Miller-Rabin

时间复杂度:O(k∗log2n) k是次数(见下文)
难度:★★★

这里只说明一下原理,关于代码,自行百度吧。

  • 根据费马小定理,随机选一个数a∈(1,p),若ap−1≡1(mod p)则很有可能是素数。多次尝试(尝试k次)若都成立若都成立则判定为素数。
  • 但是合数也有可能能通过这一测试:Carmichael数
  • Carmichael概念:
    卡迈克尔数是一种合数,使得对于所有跟n互质的整数a:an−1≡1(mod n)
  • 这种数用此方法测试时,除非random出其因子,不然都无法判断为合数。例如:6。
  • 二次探测定理:若n为素数,方程x2≡1(mod n)小于n的正整数解只有x=1和x=n−1。
  • 先计算出m、j,使得n−1=m∗2j且j尽可能大。
  • 随机选一个数a∈(1,n)
  • 计算x=ammod n
  • 然后将x不断平方j次,重复如下步骤:
    1. 计算y=x2mod n
    2. 如果y=1并且x≠1,n−1,此时一定不是素数,退出测试
    3. x=y;
    4. 如果y=1,暂时认为是素数,回到2.继续下一轮
    若上述计算中没有满足2.和4.而正常退出,即不满足an−1≡1(mod n),一定不是质数

    其次:这个博客写的很好(点击进入);

分解质因数——唯一分解定理

void divide_prime(int n)
{
    int p=2;
    int t(trunc(sqrt(n)));
    while(n!=1&&p<=t)
    {
        int i=0;
        if(n%p==0)
        {
            while(n%p==0){n/=p;i++;};
            cout<<p<<'^'<<i<<' ';
        };
        p++;
    };
    if(n!=1)cout<<n<<'^'<<1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

最大公约数和最小公倍数

gcd为最大公约数,lcm为最小公倍数
a∗b=gcd∗lcm;

int gcd(int a,int b)//注意此处a要大于b
{
    return b==0?a:gcd(b,a%b);
};
int lcm(int a,int b)
{
    return a*b/gcd(a,b);
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

扩展欧几里德

解决ax+by=gcd(a,b)
当a,b互质:ax+by=1
解为:x+(b/gcd)∗t或y+(a/gcd)∗t

int x,y;
int gcd(int a,int b)
{
    int ret,t;

    if(b==0)
    {
        x=1;
        y=0;
        return a;
    };

    ret=gcd(b,a%b);
    t=y; y=x-(a%b)*y; x=t;
    return ret;
} //不理解建议背过
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

应用举例:

逆元

所谓逆元就是ab≡1(modp),a和b就在模p的意义下逆元。

利用同余的性质:
ab≡1(modp)⇒ab−1≡0(modp)⇒ab+pt=1
故可以用扩展欧几里德求解

//其实就是NOIp2012TG D2T1/Luogu P1082
#include <iostream>
#include <cstdio>
using namespace std;
long long x,y,a,b,ans;
long long gcd(long long a,long long b)
{
    long long t,ret;
    if(b==0)
    {
        x=1;
        y=0;
        return a;
    }
    ret=gcd(b,a%b);
    t=y;
    y=x-(a/b)*y;
    x=t;
    return ret;
}
int main()
{
    cin>>a>>b;
    ans=gcd(a,b);
    while(x>b)x-=b;
    while(x<0)x+=b;
    cout<<x<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

Catalan数

原理理解:(两个应用模板)

括号化
矩阵连乘: P=a1×a2×a3×……×an,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?(h(n-1)种)

出栈次序
一个栈(无穷大)的进栈序列为1,2,3,…,n,有多少个不同的出栈序列?

详情请参照百度百科

int main() // 求第n个卡特兰数
{
    cin>>n;
    h[0]=1;h[1]=1;
    for(int i=2;i<=n;i++)
        for(int j=0;j<=i-1;j++)
            h[i]=h[i]+h[j]*h[i-j-1];
    cout<<h[n];
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

应用举例:

高精

读入、储存与输出

以下代码块均包含以下语句 :

int s[255];//255位的数
  • 1

读入与储存 :

void read()
{
    char ss[255];

    scanf("%s",ss);
    for(int i=1;i<=strlen(ss);i++)
        s[i]=ss[strlen(ss)-i+1]-'0';
    s[0]=strlen(ss);//存长度
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

输出 :

void print()
{
    for(int i=s[0];i>=1;i--)
        cout<<s[i];
};
  • 1
  • 2
  • 3
  • 4
  • 5

高精度加法

高精加单精


void add(int *s,int x)//s存高精度数,x是要加的单精度
{
    int k=1;
    s[k]+=x;
    while(s[k]>=10)
    {
        s[k+1]+=s[k]/10;
        s[k++]%=10;
    };
    s[0]=max(k,s[0]);
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

高精加高精

void add(int *s1,int *s2,int *ans)
{
    int len;
    len=max(s1[0],s2[0]);
    for(int i=1;i<=len;i++)
    {
        ans[i]+=s1[i]+s2[i];
        if(ans[i]>=10)
        {
            ans[i+1]+=ans[i]/10;
            ans[i]%=10;
        };
    };
    if(ans[len+1]!=0)len++;
    ans[0]=len;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

高精度乘法

高精乘单精

void multiply(int *s,int x)
{
    for(int i=1;i<=n;i++)
    {
        c[i]+=s[i]*x;
        c[i+1]+=(s[i]*x)/10;
        c[i]%=10;
    };
    c[0]=s[0]+1;
    while(c[c[0]]>=10)
    {
        c[c[0]+1]+=c[c[0]]/10;
        c[c[0]]%=10;
        c[0]++;
    };
    while(c[0]>1&&c[c[0]]==0)
    {
        c[0]--;
    };
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

高精乘高精

void multiply(int *s1,int *s2,int *ans)
{
    for(int i=1;i<=s1[0];i++)
        for(int j=1;j<=s2[0];j++)
        {
            ans[i+j-1]+=s1[i]*s2[j];
            ans[i+j]+=ans[i+j-1]/10;
            ans[i+j-1]%=10;
        };
    ans[0]=s1[0]+s2[0]+1;
    while(ans[0]>1&&ans[ans[0]]==0)
    {
        ans[0]--;
    };  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

高精度除法

高精度除以单精度

此处略微借鉴了一下铭哥的标程

//divide
const int opt=10000;

struct node
{
    int a[1005];
    int len;
}

node divide(node a;int p)
{
    node c;
    int i,d;

    d=0;
    c.len=a.len;
    for(int i=a.len;i>=1;i--)
    {
        d=d*opt+a.a[i];
        c.a[i]=d/p;
        d=d%p;
    };
    while(c.len>1&&c.a[c.len]==0)
    {
        c.len--;
    };
    return c;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

压位

const int opt=100000000;

void multiply(int *num,int x)
{
    for(int i=1;i<=num[0];i++)num[i]*=x;
    for(int i=1;i<=num[0];i++)
    {
        if(num[i]>=opt)
        {
            num[i+1]+=num[i]/opt;
            num[i]%=opt;
        };
        while(num[num[0]]!=0)
        {
            num[0]++;
        };
    }
}

void print(int *a)
{
    for(int i=a[0];i>=1;i--)
    {
        if(i==a[0])
        {
            cout<<a[i];
        }else
        {
            if(a[i]<10000000)cout<<0;
            if(a[i]<1000000)cout<<0;
            if(a[i]<100000)cout<<0;
            if(a[i]<10000)cout<<0;
            if(a[i]<1000)cout<<0;
            if(a[i]<100)cout<<0;
            if(a[i]<10)cout<<0;
            cout<<a[i];
        };
    };
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

注意⚠:使用压位的时候的读入不要读错
比如不要把99存到数组的两个位置里面,而应该是一个;

图论

最短路

SPFA

以下代码中包括邻接表(前向星)存图

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn=5005,maxm=100005;
const int INF=0xffffff;

int n,m; //n个点,m条边
int queue[2*maxn],head,tail;
int dist[maxn];
int firstEdge[maxn],weight[maxm],nextEdge[maxm],endPoint[maxm];
int p;
bool b[maxn];
void add(int x,int y,int z)//前向星
{
    weight[++p]=z;
    endPoint[p]=y;
    nextEdge[p]=firstEdge[x];
    firstEdge[x]=p;
};

void push(int x)  //队列的操作,详见上面
{
    b[x]=true;
    queue[++tail]=x;
}

int pop() //队列的操作,详见上面
{
    int ret;
    ret=queue[++head];
    b[ret]=false;
    return ret;
}

void SPFA()
{
    int u;
    int nowP;

    for(int i=2;i<=n;i++)dist[i]=INF;

    push(1);
    while(tail>head)
    {
        u=pop();
        nowP=firstEdge[u];
        while(nowP)
        {
            if(dist[endPoint[nowP]]>dist[u]+weight[nowP])
            {
                dist[endPoint[nowP]]=dist[u]+weight[nowP];
                if(b[endPoint[nowP]])push(endPoint[nowP]);
            };
            nowP=nextEdge[nowP];
        }
    }
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int x,y,z;
        cin>>x>>y>>z;
        add(x,y,z);
        add(y,x,z); //无向图加上这句,有向图不用加(具体看题描述)
    };
    SPFA();
    cout<<dist[n]<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

次短路

代码比较麻烦,不写了,说一下思路吧。

先用SPFA跑一遍,找出来最短路。把最短路记下来。
接下来,每次删掉最短路上的一条边,再跑一边SPFA。
运行一遍以后,路径长度最小的即次短路。

最小生成树(MST)

最小生成树即一个无向连通不含圈图中,连接G中所有点,且边集是E的子集,且边权和最小的生成树。(我解释的有点拗口)

最小生成树算法一共有两个:PrimKruskal算法,由于经并查集优化的Kruskal算法比Prim算法优秀得多,且Prim算法较容易理解,这里只给出Kruscal算法的模板。

Kruskal

下面展现两种做法,一种是普通的暴力枚举做法,另一种是并查集优化过的。并查集优化过的算法比较快,但是要忽略生成树的形状。就是说如果你需要用到新生成树的形状,那么不能使用此种方法。

  • 普通方法://类似Prim算法
struct node{int u,v,w;}e[maxe];//u是起点,v是终点,w是权
node MST[maxe];
bool com(node a,node b){return a.w<b.w;};

void Kruskal()
{
    sort(e+1,e+m+1,com);//按边权从小到大排序
    for(int i=1;i<=m;i++)
    {
        if(!b[e[i].u]&&!b[e[i].v])//b判断是否已经在集合里
            MST[++tot]=e[i];
    };
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

以上版本是自己写的,感觉不对,于是抄下来了粉书上的伪代码和讲解:

把所有边排序,记第i小的边为e[i](1<=i<m)
初始化MST为空
初始化连通分量,让每个点自成一个独立的连通分量
for(int i=1;i<=m;i++)
    if(e[i].u和e[i].v不在同一个连通分量)
    {
        把边e[i]加入MST
        合并e[i].u和e[i].v所在的连通分量
    }   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在上面的伪代码中,最关键的地方在于”连通分量的查询与合并”:需要知道任意两个点是否在同一个连通分量中,还需要合并两个连通分量。
最容易想到的方法是”暴力”——每次”合并”时只在MST中加入一条边(如果使用邻接矩阵,只需G[e[i].u][e[i].v]=1),而”查询”时直接在MST中进行图遍历(DFS和BFS都可以判断连通性)。

  • 并查集优化
int find(int x){return p[x]==x?x:p[x]=find(p[x]);}//并查集的find和路径压缩
int Kruskal()//返回的最小生成树的边权和
{
    int ans=0;
    for(int i=1;i<=n;i++)p[i]=i;//初始化并查集
    sort(edge+1,edge+m+1,com);//给边从小到大排序
    for(int i=1;i<=m;i++)
    {
        int x=find(edge[i].u);
        int y=find(edge[i].v);

        if(x!=y)
        {
            ans+=edge[i].w;//求和
            p[x]=y;
        };//如果在不同的集合,合并
    }
    return ans;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

其实此处还有一个优化,虽然不会节省很长时间,但是,优势都是一点点积累出来的!就是循环枚举边的时候不用for而用while,当当前得到的最小生成树一共有n-1条边时,最小生成树就已经生成完了,剩下的边就不用再枚举了。

训练参考:

图的遍历

Floyed

for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
        d[i][j]=INF;//INF是比最大权和大一点的数,不能超2*10^9
for(int i=1;i<=n;i++)
    d[i][i]=0;      //以上为使用前的初始化

for(int k=1;k<=n;k++)
    for(int i=1;i<=n;i++)
        for(itn j=1;j<=n;i++)
            d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

还有一点相关的东西就是传递闭包(Transitive Closure)

即在有向图中,有时不必关心路径长度,而只关心每两点间是否有通路,则可以用1和0分别表示”连通”和”不连通”。得到的结果称为有向图的传递闭包。

只需将程序中的

d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
  • 1

改成

d[i][j]=d[i][j]||(d[i][k]&&d[k][j]));
  • 1

例题:

  • 传递闭包:电话圈(Calling Circles,ACM/ICPC World Finals 1996,UVa247)(Floyd,连通分量)
  • Floyd:噪音恐惧症(Audiophobia,UVa10048)(Floyd,最大值最小路)

二分图染色

基本思路就是用DFS,对于每个点,将与其连接的下一个点染上不同的颜色。

下面的程序是“双栈排序“里二分图染色的子程序:

void dfs(int p,int colour)
 {
     if(!color[p])color[p]=colour;
     else if(color[p]!=colour)
      {
          printf("0");
          exit(0);
      }
          else return;

    for(int i=last[p];i>0;i=next[i])dfs(goal[i],3-colour);

 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

附上几篇不错的博客:(感谢喵头鹰、暗金色、LOI_summer)
喵头鹰的博客
暗金色的博客
LOI_summer的博客

建树

具体思路:对于一个节点来说,其他的任意一个节点,不是他的父节点,就是他的子节点。

传递闭包

详见上面图论部分Floyd算法。

并查集

int find(int x)//非路径压缩
{
    return p[x]==x?x:find(p[x]);
};
  • 1
  • 2
  • 3
  • 4
int find(int x)//并查集+路径压缩
{
    return p[x]==x?x:p[x]=find(p[x]);
};
  • 1
  • 2
  • 3
  • 4

LCA

倍增版LCA

思路如下

// 把树存起来(前向星,无向边)
// 把每个点的深度求出来(dfs)
// 把i向上2^j个深度的祖宗求出来(倍增)(先搜j在搜i)
// 读入两个点
// 把较深的一个点向上移动至与另一个点相同
// 如果祖宗一样就返回这个祖宗
// 两个点一起向上移(从远到近)(条件里不要忘了=)
#include <iostream>
#include <cstdio>
using namespace std;
int n,m,s;//n is node,m is qustion,s is root
int anc[500005][50];
int fa[500005];
int deep[500005];
int h[500005],v[1000010],nt[1000010];

void dfs(int u)//u代表起点
{
    int p=h[u];
    while(p)
    {
        if(deep[v[p]]==-1)//如果没处理过
        {
            deep[v[p]]=deep[u]+1;
            anc[v[p]][0]=u;//v[i]上面2^0个深度的祖先是u
            dfs(v[p]);//接下来处理一下v[i]
        }
        p=nt[p];
    }
}

void init()
{
    for(int j=1;(1<<j)<=n;j++)
        for(int i=1;i<=n;i++)
            anc[i][j]=anc[anc[i][j-1]][j-1];
}

int LCA(int a,int b)
{
    if(deep[a]<deep[b])swap(a,b);

    int i;for(i=0;(1<<i)<=deep[a];i++);i--;

    if(deep[a]!=deep[b])
        for(int j=i;j>=0;j--)
            if(deep[a]-(1<<j)>=deep[b])
                a=anc[a][j];

    if(a==b)return a;

    for(int j=i;j>=0;j--)
    {
        if(anc[a][j]&&(anc[a][j]!=anc[b][j]))
        {
            a=anc[a][j];
            b=anc[b][j];
        };
    };
    return anc[a][0];
}

int main()
{
    scanf("%d%d%d",&n,&m,&s);

    for(int i=1;i<=n;i++)deep[i]=-1;//表示deep还没有处理过

    for(int i=1;i<2*n-1;i+=2)//i个点的树有2*(n-1)条边
    {
        int a;
        scanf("%d%d",&a,&v[i]);
        nt[i]=h[a]; h[a]=i;
        v[i+1]=a; nt[i+1]=h[v[i]]; h[v[i]]=i+1;
    }//前向星村边

    deep[s]=0;//根的深度是0

    dfs(s);//深搜求每个点的深度
    init();

    for(int i=1;i<=m;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        printf("%d",LCA(a,b));
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89

动态规划

钟长者说:有几个未知量,DP数组就有几维,若求个数能再省掉最后一维。
然而这只是一般情况,例如有个例外:HAOI2012 音量调节/Luogu P1877,这道题就不能省掉最后一维。

铭哥说:重推所有的DP方程是复习DP的最佳方法

线性DP

最大递增子序列和

int ans=0,f[maxn]={-1};
for(int i=1;i<=n;i++)
    for(int j=0;j<=i-1;j++)
    {
        f[i]=max(f[i],f[j]+a[i]);
        ans=max(ans,f[i]);
    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

最大连续子序列和

f[1]=a[1];
for(int i=2;i<=n;i++)
{
    f[i]=max(f[i-1]+a[i],a[i]);
    ans=max(ans,f[i]);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最长公共自序列和

for(int i=1;i<=strlen(s1);i++)
    for(int j=1;j<=strlen(s2);j++)
        if(s1[i]==s2[j])f[i][j]=f[i-1][j-1]+1;
            else f[i][j]=max(f[i-1][j],f[i][j-1]);
  • 1
  • 2
  • 3
  • 4

字符串转换问题

给定A串和B串,有删除一个字符、插入一个字符、改变一个字符三种操作,求A变到B的最少操作次数。
f[i][j]表示A的前i个字符变成B的前j个字符所用的最少步数。

for(int i=0;i<=strlen(A);i++)f[i][0]=i;
for(int i=0;i<=strlen(B);i++)f[0][i]=i;
for(int i=1;i<=strlen(A);i++)
    for(int j=1;j<=strlen(B);j++)
        if(A[i]==B[j])f[i][j]=f[i-1][j-1];
            else f[i][j]=min(min(f[i][j-1],f[i-1][j]),f[i-1,j-1])+1;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最长不下降子序列

//据说可以用二分进行优化
for(int i=2;i<=n;i++)
    for(int j=1;j<=i-1;j++)
        if(a[j]<=a[i])f[i]=max(f[i],f[j]+1);
for(int i=1;i<=n;i++)ans=max(ans,f[i]);
  • 1
  • 2
  • 3
  • 4
  • 5

背包DP

01背包

for(int i=1;i<=m;i++)//m个物品
    for(int j=1;j<=w;j++)//背包容量为w
        if(a[i]<=j)
        {
            dp[i][j]=max(dp[i-1,j-a[i]]+val[i],dp[i-1,j]);//a数组是占用的容量,val是价值
        }else
        {
            dp[i][j]=dp[i-1][j];
        };
//此时dp[m][w]即为最大权值
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

优化:

//条件和上面一样
for(int i=1;i<=n;i++)
    for(int j=w;i>=a[i];j--)
        dp[j]=(dp[j],dp[j-a[i]]+val[i]);
  • 1
  • 2
  • 3
  • 4

例题:

完全背包

//条件和上面一样,只是每个物品可以取无数次
for(int i=1;i<=n;i++)
    for(int j=a[i];i<=w;j++)//注意这里的改动
        dp[j]=(dp[j],dp[j-a[i]]+val[i]);
  • 1
  • 2
  • 3
  • 4

混合背包

即有好几种背包的条件,分别写dp满足条件就可以了(比如NOIp2014TG的飞扬的小鸟)

for(int k=1;k<=组数;k++)
    for(int j=c;j>=0;j++)
        for(int i=1;i<=n;i++)
            if(j>=v[i])f[j]=maxn(f[j],f[j-v[i]]+w[i]);
  • 1
  • 2
  • 3
  • 4

例题:

  • NOIp2014TG 飞扬的小鸟

分组背包

这里有一篇分组背包博客写的不错,参考这篇博客吧。感谢博客的作者nywsp。

其他模板

归并排序

void mergesort(int l,int r)
{
    if(l==r)return;
    int ret[maxn];
    int mid;

    mid=(l+r)/2;
    mergesort(l,mid); mergesort(mid+1,r);

    int i=l,j=mid+1,k=l;
    while(i<=mid&&j<=r)
    {
        if(a[i]<a[j])
        {
            ret[k++]=a[i++];
        }else
        {
            ret[k++]=a[j++];
            ans+=mid-i+1;//求逆序对时加上这一句
        };
    };
    while(i<=mid)ret[k++]=a[i++];
    while(j<=r)ret[k++]=a[j++];

    for(int i=l;i<=r;i++)a[i]=ret[i];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

二分

void binary(int l,int r)    //找最小
{
    while(r>l)
    {
        mid=(l+r)/2;
        if(check(mid))r=mid;
            else l=mid+1;
    };
    ans=l;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
void binary(int l,int r)    //找最大
{
    while(r>l)
    {
        mid=(r+l)/2+1;  //特别注意
        if(check(mid))l=mid+1;
            else r=mid;
    };
    ans=l;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Hash

哈希大法好

#define ull unsigned long long
ull hash(char *s)
{
    int l=strlen(S);
    ull ans=0;
    int seed=27;
    for(int a=0;a<l;a++)
        ans=ans*seed+a[i]-'a'+1;
    return ans;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

感谢您的阅读