算法学习笔记

文章目录


前言

算法学习个人笔记


一、基础算法

二、数据结构

1.链表

1.1 单链表

acwing 826

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int head,e[N],ne[N],idx;
void init()
{
    head=-1;
    idx=0;
}
void add_to_head(int x)
{
    e[idx]=x;
    ne[idx]=head;
    head=idx++;//模拟的head指针会变
}
void add(int k,int x)
{
    e[idx]=x;
    ne[idx]=ne[k];
    ne[k]=idx++;
}
void remove(int k)
{
    ne[k]=ne[ne[k]];
}
int main()
{
    int m;
    cin>>m;
    init();
    while(m--)
    {
        char op;
        cin>>op;
        if(op=='H')
        {
            int x;
            cin>>x;
            add_to_head(x);
        }
        else if(op=='I')
        {
            int k,x;
            cin>>k>>x;
            add(k-1,x);
        }
        else
        {
            int k;
            cin>>k;
            if(!k)head=ne[head];
            remove(k-1);
        }
        //cout<<op<<endl;
    }
    for(int i=head;i!=-1;i=ne[i])cout<<e[i]<<" ";//遍历
    cout<<endl;
}

1.2 双链表

acwing 827

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int e[N],l[N],r[N],idx;
void init()
{
    //0表示左端点,1表示右端点
    r[0]=1;
    l[1]=0;
    idx=2;
}
void add(int k,int x)
{
    e[idx]=x;
    l[idx]=k;
    r[idx]=r[k];
    l[r[k]]=idx;
    r[k]=idx;
    idx++;
}
void remove(int k)
{
    l[r[k]]=l[k];
    r[l[k]]=r[k];
}
int main()
{
    int m;
    cin>>m;
    init();
    while(m--)
    {
        string op;
        cin>>op;
        if(op=="L")
        {
            int x;
            cin>>x;
            add(0,x);
        }
        else if(op=="R")
        {
            int x;
            cin>>x;
            add(l[1],x);
        }
        else if(op=="D")
        {
            int k;
            cin>>k;
            remove(k+1);
        }
        else if(op=="IL")
        {
            int k,x;
            cin>>k>>x;
            add(l[k+1],x);
        }
        else if(op=="IR")
        {
            int k,x;
            cin>>k>>x;
            add(k+1,x);
        }
        //cout<<op<<endl;
    }
    for(int i=r[0];i!=1;i=r[i])cout<<e[i]<<" ";
    cout<<endl;
    return 0;
}

2.栈

2.1 数组模拟栈

acwing 828

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int stk[N],tt;
void add(int x)
{
    stk[tt++]=x;
}
void pop()
{
    tt--;
}
int top()
{
    return stk[tt-1];
}
bool empty()
{
    if(tt>0)return 0;
    else return 1;
}
int main()
{
    int m;
    cin>>m;
    while(m--)
    {
        string s;
        cin>>s;
        int x;
        if(s=="push")
        {
            cin>>x;
            add(x);
        }
        else if(s=="query")
        {
            cout<<top()<<endl;
        }
        else if(s=="pop")
        {
            pop();
        }
        else 
        {
            if(empty())
                cout<<"YES"<<endl;
            else cout<<"NO"<<endl;
        }
    }
    return 0;
}
例题

acwing 3302

#include<bits/stdc++.h>
using namespace std;
stack<char>op;
stack<int>num;
void eval()
{
    int b = num.top(); num.pop();
    int a = num.top(); num.pop();
    char c = op.top(); op.pop();

    int x;
    if(c == '+') x = a + b;
    else if(c == '-') x = a - b;
    else if(c == '*') x = a * b;
    else x = a / b;

    num.push(x);
}
int main()
{
    string s;
    cin>>s;
    unordered_map<char,int>pr{{'+',1},{'-',1},{'*',2},{'/',2}};
    for(string::size_type i=0;i<s.size();i++)
    {
        if(isdigit(s[i]))
        {
            int x=0;
            string::size_type j=i;
            while(j<s.size()&&isdigit(s[j]))x=10*x+s[j++]-'0';
            i=j-1;
            num.push(x);
        }
        else if(s[i]=='(')op.push(s[i]);
        else if(s[i]==')')
        {
            while(op.size()&&op.top()!='(')eval();
            op.pop();
        }
        else 
        {
            while(op.size()&&pr[op.top()]>=pr[s[i]])eval();
            op.push(s[i]);
        }
    }
    while(op.size())eval();
    cout<<num.top()<<endl;
    return 0;
}

2.2 单调栈

acwing 830

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int stk[N],tt;
int main()
{
    int n;
    cin>>n;
    int x;
    for(int i=0;i<n;i++)
    {
        cin>>x;
        while(tt&&stk[tt]>=x)tt--;
        if(tt)cout<<stk[tt]<<" ";
        else cout<<-1<<" ";
        stk[++tt]=x;
    }
    return 0;
}

3.队列

3.1 数组模拟队列

acwing 829

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int q[N],tt=-1,hh;
void add(int x)
{
    q[++tt]=x;
}
void pop()
{
    hh++;
}
int top()
{
    return q[hh];
}
bool empty()
{
    if(hh<=tt)return 0;
    else return 1;
}
int main()
{
    int m;
    cin>>m;
    while(m--)
    {
        string s;
        cin>>s;
        int x;
        if(s=="push")
        {
            cin>>x;
            add(x);
        }
        else if(s=="query")
        {
            cout<<top()<<endl;
        }
        else if(s=="pop")
        {
            pop();
        }
        else 
        {
            if(empty())cout<<"YES"<<endl;
            else cout<<"NO"<<endl;
        }
    }
    return 0;
}

3.2 单调队列

acwing 154

#include<bits/stdc++.h>
using namespace std;
const int N=1000010;
int a[N],q[N];
int main()
{
    int n,k;
    cin>>n>>k;
    for(int i=0;i<n;i++)cin>>a[i];
    int tt=-1,hh=0;
    for(int i=0;i<n;i++)
    {
        if(hh<=tt&&i-k+1>q[hh])hh++;//判断队头是否滑出窗口
        while(hh<=tt&&a[q[tt]]>=a[i])tt--;
        q[++tt]=i;
        if(i>=k-1)cout<<a[q[hh]]<<" ";
    }
    cout<<endl;
    tt=-1,hh=0;
    for(int i=0;i<n;i++)
    {
        if(hh<=tt&&i-k+1>q[hh])hh++;//判断队头是否滑出窗口
        while(hh<=tt&&a[q[tt]]<=a[i])tt--;
        q[++tt]=i;
        if(i>=k-1)cout<<a[q[hh]]<<" ";
    }
    return 0;
}

4.KMP

acwing 831

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

const int N = 100010, M = 1000010;

int n, m;
int ne[N];
char s[M], p[N];

int main()
{
    cin >> n >> p + 1 >> m >> s + 1;
    //求next数组
    //next数组的含义:对next[ j ] ,是p[ 1, j ]串中前缀和后缀相同的最大长度(部分匹配值),即 p[1,next[j]] = p[j-next[j]+1,j]。
    for(int i=2,j=0;i<=n;i++)
    {
        while(j&&p[i]!=p[j+1])j=ne[j];
        if(p[i]==p[j+1])j++;
        ne[i]=j;//匹配成功后赋值
    }
    for(int i=1,j=0;i<=m;i++)
    {
        while(j&&s[i]!=p[j+1])j=ne[j];
        if(s[i]==p[j+1])j++;
        if(j==n)
        {
            cout<<i-n<<' ';
            j=ne[j];
        }
    }

    return 0;
}

5.Trie

5.1 Trie字符串统计

acwing 835

#include<bits/stdc++.h>
using namespace std;
const int N=10010;
int son[N][26],cnt[N],idx;
char ch[N];
//x
//son[x][0] x的第0个儿子,son[x][1] x的第一个儿子 
//cnt[x] 以x结尾的单词有多少个
void insert(char s[])
{
    int p=0;
    for(int i=0;s[i];i++)
    {
        int x=s[i]-'a';
        if(!son[p][x])son[p][x]=++idx;
        p=son[p][x];
    }
    cnt[p]++;
}
int query(char s[])
{
    int p=0;
    for(int i=0;s[i];i++)
    {
        int x=s[i]-'a';
        if(!son[p][x])return 0;
        p=son[p][x];
    }
    return cnt[p];
}
int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        char op;
        cin>>op>>ch;
        if(op=='I')
        {
            insert(ch);
        }
        else
        cout<<query(ch)<<endl;
    }
    return 0;
}

5.2 最大异或对

Trie树也可以存二进制数
acwing 143

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
const int M=31*N;
int a[N],son[M][2],idx;
void insert(int x)
{
    int p=0;
    for(int i=30;i>=0;i--)
    {
        int u=x>>i&1;//取x的第i位二进制数
        if(!son[p][u])son[p][u]=++idx;
        p=son[p][u];
    }
}
int search(int x)
{
    int p=0;
    int res=0;
    for(int i=30;i>=0;i--)
    {
        int u=x>>i&1;
        if(son[p][!u])
        {
            res=res*2+1;
            p=son[p][!u];
        }
        else
        {
            res=res*2+0;
            p=son[p][u];
        }

    }
    return res;
}
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
        insert(a[i]);
    }
    int res=0;
    for(int i=0;i<n;i++)
    {
        res=max(res,search(a[i]));
    }
    cout<<res<<endl;
    return 0;
}

6.并查集

acwing 836

开始时每个集合都是一个独立的集合,并且都是等于自己本身下标的数
例如:
p[5]=5,p[3]=3;
如果是M操作的话那么就将集合进行合并,合并的操作是:
p[3]=p[5]=5;
所以3的祖宗节点便成为了5
此时以5为祖宗节点的集合为{5,3}
如果要将p[9]=9插入到p[3]当中,应该找到3的祖宗节点,
然后再把p[9]=9插入其中,所以p[9]=find(3);(find()函数用于查找祖宗节点)
也可以是p[find(9)]=find(3),因为9的节点本身就是9
此时以5为祖宗节点的集合为{5,3,9};
如果碰到多个数的集合插入另一个集合当中其原理是相同的
例如:
上述中以5为祖宗节点的是p[5],p[3],p[9];(即p[5]=5,p[3]=5,p[9]=5)
再构造一个以6为祖宗节点的集合为{6,4,7,10}
如果要将以6为祖宗节点的集合插入到以5为祖宗节点的集合,则该操作可以是
p[6]=find(3)(或者find(9),find(5))
此时p[6]=5
当然如果是以6为祖宗节点集合中的4,7,10则可以这样
p[find(4)]=find(3)
或者p[find(7)]=find(3)均可以
此时以6为祖宗节点的集合的祖宗节点都成为了5
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
int p[N];
int find(int x)
{
    if(p[x]!=x)
    {
        p[x]=find(p[x]);
    }
    return p[x];
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)p[i]=i;
    while(m--)
    {
        string op;
        int a,b;
        cin>>op>>a>>b;
        if(op=="M")
        {
            p[find(a)]=find(b);
        }
        else
        {
            //cout<<find(a)<<" "<<find(b)<<endl;
            if(p[find(a)]==p[find(b)])puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

acwing 837

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int p[N],sizes[N];
int n,m;
int find(int x)
{
    if(p[x]!=x)p[x]=find(p[x]);
    return p[x];
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        p[i]=i;
        sizes[i]=1;
    }
    while(m--)
    {
        string op;
        int a,b;
        cin>>op;
        if(op=="C")
        {
            cin>>a>>b;
            if(find(a)==find(b))continue;//特判
            sizes[find(b)]+=sizes[find(a)];
            p[find(a)]=find(b);//二者顺序不可调换
        }
        else if(op=="Q1")
        {
            cin>>a>>b;
            if(p[find(a)]==p[find(b)])puts("Yes");
            else puts("No");
        }
        else
        {
            cin>>a;
            cout<<sizes[find(a)]<<endl;
        }
    }
    return 0;
}

7.堆

7.1 堆排序

acwing 838

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int h[N],sizes;
int n,m;
void down(int u)
{
    int t=u;
    if(u*2<=sizes&&h[u*2]<h[t])t=u*2;
    if(u*2+1<=sizes&&h[u*2+1]<h[t])t=u*2+1;
    if(t!=u)
    {
        swap(h[u],h[t]);
        down(t);
    }
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>h[i];
        sizes++;
    }
    for(int i=n/2;i;i--)down(i);//建堆
    while(m--)
    {
        cout<<h[1]<<' ';
        h[1]=h[sizes];
        sizes--;
        down(1);
    }
    return 0;
}

7.2 带映射的堆

acwing 839

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int h[N],ph[N],hp[N],sizes;//ph[m] = k(第m个插入的下标),ph数组主要用于帮助从m映射到下标k,hp数组方便查找m(反函数)
void heap_swap(int a,int b)
{
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a],hp[b]);
    swap(h[a],h[b]);
}
void down(int u)
{
    int t=u;
    if(u*2<=sizes&&h[u*2]<h[t])t=u*2;
    if(u*2+1<=sizes&&h[u*2+1]<h[t])t=u*2+1;
    if(t!=u)
    {
        heap_swap(t,u);
        down(t);
    }
}
void up(int u)
{
    int t=u;
    if(u/2&&h[u/2]>h[t])t=u/2;
    if(t!=u)
    {
        heap_swap(t,u);
        up(t);
    }
}
int main()
{
    int n,m=0;
    cin>>n;
    while(n--)
    {
        string op;
        cin>>op;
        int k,x;
        if(op=="I")
        {
            cin>>x;
            sizes++;
            m++;
            h[sizes]=x;
            ph[m]=sizes,hp[sizes]=m;
            up(sizes);
        }
        else if(op=="D")
        {
            cin>>k;
            k=ph[k];
            heap_swap(k,sizes);
            sizes--;
            down(k);
            up(k);
        }
        else if(op=="PM")
        {
            cout<<h[1]<<endl;
        }
        else if(op=="DM")
        {
            heap_swap(1,sizes);
            sizes--;
            down(1);
        }
        else if(op=="C")
        {
            cin>>k>>x;
            k=ph[k];
            h[k]=x;
            down(k);
            up(k);
        }
    }
    return 0;
}

8.哈希表

8.1 模拟散链表

acwing 840

8.1.1 开放寻址法
//开放寻址法
#include<bits/stdc++.h>
using namespace std;
const int N=200003,null=0x3f3f3f3f;
int h[N];
int find(int x);
void insert(int x)
{
    int k=find(x);
    h[k]=x;
}
int find(int x)
{
    int k=(x%N+N)%N;
    while(h[k]!=null&&h[k]!=x)
    {
        k++;
        if(k==N)k=0;//防止数组越界
    }
    return k;
}
int main()
{
    memset(h,0x3f,sizeof(h));//按字节赋值,每个字节为0x3f
    int n;
    cin>>n;
    while(n--)
    {
        string op;
        int x;
        cin>>op>>x;
        if(op=="I")insert(x);
        else
        {
            if(h[find(x)]!=null)puts("Yes");
            else puts("No");
        }
    }
    return 0;
}
8.1.2 拉链法
//拉链法
//若要删除直接在节点处打标记
#include<bits/stdc++.h>
using namespace std;
const int N=100003;
int h[N],e[N],ne[N],idx;
void insert(int x)
{
    int k=(x%N+N)%N;
    e[idx]=x;
    ne[idx]=h[k];
    h[k]=idx++;

}
bool query(int x)
{
    int k=(x%N+N)%N;;
    for(int i=h[k];i!=-1;i=ne[i])
        if(x==e[i])return 1;
    return 0;
}
int main()
{
    memset(h,-1,sizeof(h));
    int n;
    cin>>n;
    while(n--)
    {
        string op;
        int x;
        cin>>op>>x;
        if(op=="I")insert(x);
        else
        {
            if(query(x))puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

8.2 字符串哈希

acwing 841

#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N=100010,P=131;
ULL h[N],p[N];
int get(int l,int r)
{
    return h[r]-h[l-1]*p[r-l+1];
}
int main()
{
    int n,m;
    char str[N];
    cin>>n>>m>>str+1;
    p[0]=1;
    for(int i=1;i<=n;i++)
    {
        p[i]=p[i-1]*P;
        h[i]=h[i-1]*P+str[i];//h[i]等于h[i-1]左移一位在加上str[i]的映射(可以直接取str[i],但不能为0)
    }
    while(m--)
    {
        int l1,r1,l2,r2;
        cin>>l1>>r1>>l2>>r2;
        if(get(l1,r1)==get(l2,r2))puts("Yes");
        else puts("No");
    }
    return 0;
}

9.树状数组

落谷P3374
数组记录区间和,下标 i i i若代表 [ l , r ] [l,r] [l,r]区间的和,则左儿下标为 2 i 2i 2i,代表 [ l , ( l + r ) > > 1 ] [l,(l+r)>>1] [l,(l+r)>>1]区间的和,右儿子下标为 2 i + 1 2i+1 2i+1,代表区间 [ ( l + r ) > > 1 + 1 , r ] [(l+r)>>1+1,r] [(l+r)>>1+1,r]的和。

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10,M=3e6+10;
int f[M];
int a[N];
int n,m;
void BuildTree(int k,int l,int r)
{
    if(l==r)
    {
        f[k]=a[l];
        return;
    }
    int m=(l+r)>>1;
    BuildTree(2*k,l,m);
    BuildTree(2*k+1,m+1,r);
    f[k]=f[2*k]+f[2*k+1];
}
void add(int k,int l,int r,int x,int y)//区间[l,r],下标为x的数加y
{
    f[k]+=y;
    if(l==r)
    {
        return;
    }
    int m=(l+r)>>1;
    if(x<=m)add(k*2,l,m,x,y);
    else add(k*2+1,m+1,r,x,y);
}
int cal(int k,int l,int r,int s,int t)
{
    if(l==s&&r==t)return f[k];
    int m=(l+r)>>1;
    if(t<=m)return cal(k*2,l,m,s,t);//区间[s,t]完全在左儿子[l,m]中
    else
        if(s>m)return cal(k*2+1,m+1,r,s,t);//区间[s,t]完全在右儿子[m+1,r]中
        else return cal(k*2,l,m,s,m)+cal(k*2+1,m+1,r,m+1,t);//区间[s,t]夹在左儿子[l,m]和右儿子[m+1,r]中间
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    BuildTree(1,1,n);
    while(m--)
    {
        int op;
        cin>>op;
        if(op==2)
        {
            int l,r;
            cin>>l>>r;
            cout<<cal(1,1,n,l,r)<<endl;
        }
        if(op==1)
        {
            int x,y;
            cin>>x>>y;
            add(1,1,n,x,y);
        }
    }
    return 0;
}

10.线段树

落谷P3372

10.1 标记不下传

线段树入门教学视频 P2 带标记的线段树
数组记录区间和,下标 i i i若代表 [ l , r ] [l,r] [l,r]区间的和,则左儿下标为 2 i 2i 2i,代表 [ l , ( l + r ) > > 1 ] [l,(l+r)>>1] [l,(l+r)>>1]区间的和,右儿子下标为 2 i + 1 2i+1 2i+1,代表区间 [ ( l + r ) > > 1 + 1 , r ] [(l+r)>>1+1,r] [(l+r)>>1+1,r]的和。
v[k]代表当前编号为k的区间每一个数要加v[k],f[k]表示排除了根到当前编号为k的节点的路径的v的影响,当前编号为k的子树的数字的和

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
typedef long long ll;
ll f[4*N],v[4*N];
int a[N];
inline void buildTree(int k,int l,int r)
{
    v[k]=0;
    if(l==r)
    {
        f[k]=a[l];
        return;
    }
    int m=(l+r)>>1;
    buildTree(2*k,l,m);
    buildTree(2*k+1,m+1,r);
    f[k]=f[2*k]+f[2*k+1];
}
inline void insert(int k,int l,int r,int x,int y,ll z)
{
    if(l==x&&r==y)
    {
        v[k]+=z;
        return;
    }
    f[k]+=(y-x+1)*z;
    int m=(l+r)>>1;
    if(y<=m)
    {
        insert(2*k,l,m,x,y,z);
    }
    else
    {
        if(x>m)
        {
            insert(2*k+1,m+1,r,x,y,z);
        }
        else
        {
            insert(2*k,l,m,x,m,z),insert(2*k+1,m+1,r,m+1,y,z);
        }
    }
}
ll calc(int k,int l,int r,int x,int y,ll p)
{
    p+=v[k];
    if(l==x&&r==y)
    {
        return f[k]+p*(r-l+1);
    }
    int m=(l+r)>>1;
    if(y<=m)
    {
        return calc(2*k,l,m,x,y,p);
    }
    else
    {
        if(x>m)
        {
            return calc(2*k+1,m+1,r,x,y,p);
        }
        else
        {
            return calc(2*k,l,m,x,m,p)+calc(2*k+1,m+1,r,m+1,y,p);
        }
    }
}
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    buildTree(1,1,n);
    while(m--)
    {
        int op;
        cin>>op;
        if(op==1)
        {
            int x,y;
            ll k;
            cin>>x>>y>>k;
            insert(1,1,n,x,y,k);
        }
        else
        {
            int x,y;
            cin>>x>>y;
            cout<<calc(1,1,n,x,y,0)<<endl;
        }
    }
    return 0;
}

10.2 标记下传

线段树入门教学视频 P3 标记下传
将当前节点k的v下传给k的左右儿子

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
typedef long long ll;
ll f[4*N],v[4*N];
int n,m;
int a[N];
inline void buildTree(int k,int l,int r)
{
    v[k]=0;
    if(l==r)
    {
        f[k]=a[l];
        return;
    }
    int m=(l+r)>>1;
    buildTree(2*k,l,m);
    buildTree(2*k+1,m+1,r);
    f[k]=f[2*k]+f[2*k+1];
}
inline void insert(int k,int l,int r,int x,int y,int z)
{
    if(l==x&&r==y)
    {
        v[k]+=z;
        return;
    }
    if(v[k])
    {
        v[k*2]+=v[k],v[2*k+1]+=v[k],v[k]=0;
    }
    int m=(l+r)>>1;
    if(y<=m)
    {
        insert(2*k,l,m,x,y,z);
    }
    else
    {   
        if(x>m)insert(2*k+1,m+1,r,x,y,z);
        else
        {
            insert(2*k,l,m,x,m,z),insert(2*k+1,m+1,r,m+1,y,z);
        }
    }
    f[k]=f[2*k]+v[2*k]*(m-l+1)+f[2*k+1]+v[2*k+1]*(r-m);//更新
}
ll calc(int k,int l,int r,int x,int y)
{
    if(l==x&&r==y)
    {
        return f[k]+(r-l+1)*v[k];
    }
    if(v[k])
    {
        v[k*2]+=v[k],v[2*k+1]+=v[k],v[k]=0;
    }
    int m=(l+r)>>1;
    ll res=0;//f[k]还未更新,不能直接return,先拿临时变量记录一下
    if(y<=m)
    {
        res=calc(2*k,l,m,x,y);
    }
    else
    {
        if(x>m)
        {
            res=calc(2*k+1,m+1,r,x,y);
        }
        else
        {
            res=calc(2*k,l,m,x,m)+calc(2*k+1,m+1,r,m+1,y);
        }
    }
    f[k]=f[2*k]+v[2*k]*(m-l+1)+f[2*k+1]+v[2*k+1]*(r-m);//更新
    return res;
}
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    buildTree(1,1,n);
    while(m--)
    {
        int op;
        cin>>op;
        if(op==1)
        {
            int x,y;
            ll k;
            cin>>x>>y>>k;
            insert(1,1,n,x,y,k);
        }
        else
        {
            int x,y;
            cin>>x>>y;
            cout<<calc(1,1,n,x,y)<<endl;
        }
    }
    return 0;
}

三、图论

1.最短路问题

1.1 dijkstra–适用于所有边权为正数

1.1.1 朴素版 O ( n 2 ) O(n^2) O(n2)–适用于稠密图

思路:邻接矩阵维护图,st数组判断是否确定最短路,循环n次,每次找到还未确定最短路的点中路径最短的点t,标记该点(放入st数组中),然后利用该点t依次更新每个点所到相邻的点路径值
acwing 849

//稠密图O(n^2)
#include<bits/stdc++.h>
using namespace std;
const int N=510;
int g[N][N];
int dist[N];//用于存储每个点到起点的最短距离
bool st[N];//用于在更新最短距离时 判断当前的点的最短距离是否确定 是否需要更新
int n,m;
int dijkstra()
{
    memset(dist,0x3f,sizeof(dist));
    dist[1]=0;
    for(int i=0;i<n;i++)
    {
        int t=-1;//将t设置为-1 因为Dijkstra算法适用于不存在负权边的图
        for(int j=1;j<=n;j++)
        {
            if(!st[j]&&(t==-1||dist[t]>dist[j]))//该步骤即寻找还未确定最短路的点中路径最短的点
            {
                t=j;//找不在st数组中的距离最近的点
            }
        }
        st[t]=true;//标记确定了最短路的点中路径最短的点
        for(int j=1;j<=n;j++)
        {
            dist[j]=min(dist[j],dist[t]+g[t][j]);//依次更新每个点所到相邻的点路径值
        }
    }
    if(dist[n]==0x3f3f3f3f)
    {
        return -1;
    }
    return dist[n];
}
int main()
{
    memset(g,0x3f,sizeof(g));
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        g[a][b]=min(g[a][b],c);//如果发生重边的情况则保留最短的一条边
    }
    cout<<dijkstra()<<endl;
    return 0;
}
1.1.2 堆优化 O ( m l o g n ) O(mlogn) O(mlogn)–适用于稀疏图

思路:链式前向星维护图,利用堆维护未确定最短路的点中路径最短的点
acwing 850

//稀疏图 堆优化dijkstra O(mlogn)
#include<bits/stdc++.h>
using namespace std;
const int N=150010;
typedef pair<int,int>PII;
int n,m;
int h[N],e[N],ne[N],w[N],idx;
int dist[N];
bool st[N];
void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int dijkstra()
{
    memset(dist,0x3f,sizeof(dist));
    dist[1]=0;
    priority_queue<PII,vector<PII>,greater<PII>>pq;
    pq.push({0,1});
    while(pq.size())
    {
        auto t=pq.top();//代表未确定最短路的点中路径最短的点(不在st数组中的距离最近的点)
        pq.pop();
        int ver=t.second,distance=t.first;
        if(st[ver])continue;
        st[ver]=true;
        for(int i=h[ver];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dist[j]>distance+w[i])
            {
                dist[j]=distance+w[i];
                pq.push({dist[j],j});
            }
        }
    }
    if(dist[n]==0x3f3f3f3f)
        return -1;
    return dist[n];
}
int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof(h));
    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    cout<<dijkstra()<<endl;
    return 0;
}

1.2 bellman-ford O ( n m ) O(nm) O(nm)–适用于存在负权边,有边数限制的最短路

思路:普通结构体存图(a->b边权为w),循环k次,备份上一次遍历的dist数组,for遍历m次,更新最短路dist[b]=min(dist[b],backup[a]+w)
在下面代码中,判断从a到b是否是无穷大距离时,需要进行if(t > INF/2)判断,而并非是if(t == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,t大于某个与INF相同数量级的数即可
acwing 853

#include<bits/stdc++.h>//O(nm)
using namespace std;
const int N=510,M=100010;
int dist[N],backup[N];
struct edge
{
    int a,b,w;
}edges[M];
int n,m,k;//边数限制为k的最短路
void bellman_ford()
{
    memset(dist,0x3f,sizeof(dist));
    dist[1]=0;
    for(int i=0;i<k;i++)//循环k次
    {
        memcpy(backup,dist,sizeof(dist));//备份上一次的dist数组
        for(int j=0;j<m;j++)
        {
            int a=edges[j].a,b=edges[j].b,w=edges[j].w;
            dist[b]=min(dist[b],backup[a]+w);
        }
    }
}
int main()
{
    cin>>n>>m>>k;
    for(int i=0;i<m;i++)
    {
        int a,b,w;
        cin>>a>>b>>w;
        edges[i]={a,b,w};
    }
    bellman_ford();
    if (dist[n] > 0x3f3f3f3f / 2) cout<<"impossible"<<endl;//可能存在负权边
    else cout<<dist[n]<<endl;

    return 0;
}

1.3 spfa–适用于存在负权边 一般为 O ( m ) O(m) O(m),最坏为 O ( n m ) O(nm) O(nm)

1.3.1 spfa求最短路

思路:链式前向星维护图,st数组判断是否在队列中,起点s入队,st[s]=true,队列不为空一直循环,取队头,队头t出队,st[t]=false,利用t依次更新所有出边t->j,若dist[j]>dist[t]+w[i],dist[j] = dist[t] + w[i],若更新了并且st[j]==false,j入队,st[j]=true
由于不限制次数,最短路不会是INF量级,不需要进行if(t > INF/2)判断,直接进行if(t ==INF)判断就行
acwing 851

#include<bits/stdc++.h>//一般为O(m),最坏O(nm)
using namespace std;
queue<int>q;
const int N=100010;
int h[N],e[N],ne[N],w[N],idx;
int dist[N];
bool st[N];//st数组存当前这个点是否在队列当中
void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int n,m;
int spfa()
{
    memset(dist,0x3f,sizeof(dist));
    dist[1]=0;
    q.push(1);
    st[1]=true;
    while(q.size())
    {
        int t=q.front();
        q.pop();
        st[t]=false;
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dist[j]>dist[t]+w[i])
            {
                dist[j]=dist[t]+w[i];
                if(!st[j])
                {
                    q.push(j);
                    st[j]=true;
                }
            }
        }
    }
    return dist[n];
}
int main()
{
    memset(h,-1,sizeof(h));
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    int res=spfa();
    if(res==0x3f3f3f3f)
        cout<<"impossible"<<endl;
    else cout<<res<<endl;
    return 0;
}
1.3.2 spfa判断负环

思路:链式前向星维护图,st数组判断是否在队列中,cnt数组记录最短路长度,所有点i入队,st[i]=true,队列不为空一直循环,取队头,队头t出队,st[t]=false,利用t依次更新所有出边t->j,若dist[j]>dist[t]+w[i],dist[j] = dist[t] + w[i],cnt[j]=cnt[t]+1,若更新了并且cnt[j]>=n,存在负环,若更新了并且st[j]==false,j入队,st[j]=true
acwing 852

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int h[N],e[N],w[N],ne[N],idx;
void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int dist[N];
int cnt[N];//记录最短路的长度
bool st[N];
int n,m;
bool spfa()
{
    queue<int>q;
    for(int i=1;i<=n;i++)//开始时放入所有点
    {
        q.push(i);
        st[i]=true;
    }
    while(q.size())
    {
        int t=q.front();
        q.pop();
        st[t]=false;
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dist[j]>dist[t]+w[i])
            {
                dist[j]=dist[t]+w[i];
                cnt[j]=cnt[t]+1;
                if(cnt[j]>=n)return true;//1到j的最短路有n条边,n+1个顶点,图中只有n个顶点
                //由抽屉原理一定有负环(之间存在i->i的环,且一定为负环,若为正环不会走)
                if(!st[j])
                {
                    q.push(j);
                    st[j]=true;
                }
            }
        }
    }
    return false;
}
int main()
{
    memset(h,-1,sizeof(h));
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    if(spfa())cout<<"Yes"<<endl;
    else cout<<"No"<<endl;
    return 0;
}

1.4 floyd O ( n 3 ) O(n^3) O(n3)–适用于多源汇最短路

思路: f [ i , j , k ] f[i, j, k] f[i,j,k]表示从i走到j的路径上除i和j点外只经过1到k的点的所有路径的最短距离。那么 f [ i , j , k ] = m i n ( f [ i , j , k − 1 ) , f [ i , k , k − 1 ] + f [ k , j , k − 1 ] f[i, j, k] = min(f[i, j, k - 1), f[i, k, k - 1] + f[k, j, k - 1] f[i,j,k]=min(f[i,j,k1),f[i,k,k1]+f[k,j,k1]
因此在计算第k层的f[i, j]的时候必须先将第k - 1层的所有状态计算出来,所以需要把k放在最外层。
读入邻接矩阵,将次通过动态规划装换成从i到j的最短距离矩阵
在下面代码中,判断从a到b是否是无穷大距离时,需要进行if(t > INF/2)判断,而并非是if(t == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,t大于某个与INF相同数量级的数即可
acwing 854

#include<bits/stdc++.h>//O(n^3)
using namespace std;
const int N=210;
const int INF=0x3f3f3f3f;
int dist[N][N];
int n,m,k;
void floyd()
{
    for(int k=1;k<=n;k++)
    {
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=n;j++)
            {
                dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j]);
            }
        }
    }
}
int main()
{
    cin>>n>>m>>k;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            if(i==j)dist[i][j]=0;
            else dist[i][j]=INF;
        }
    }
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        dist[a][b]=min(dist[a][b],c);
    }
    floyd();
    for(int i=0;i<k;i++)
    {
        int x,y;
        cin>>x>>y;
        if(dist[x][y]>INF/2)cout<<"impossible"<<endl;//可能有负权边存在
        else cout<<dist[x][y]<<endl;
    }
    return 0;
}

2.最小生成树

2.1 prim

prim 算法干的事情是:给定一个无向图,在图中选择若干条边把图的所有节点连起来。要求边长之和最小。在图论中,叫做求最小生成树。
prim 算法采用的是一种贪心的策略。
每次将离连通部分的最近的点和点对应的边加入的连通部分,连通部分逐渐扩大,最后将整个图连通起来,并且边长之和最小。
思路:S:当前已经在联通块中的所有点的集合

  1. dist[i] = inf
  2. for n 次
    t<-S外离S最近的点
    利用t更新S外点到S的距离
    st[t] = true
    n次迭代之后所有点都已加入到S中
    联系:Dijkstra算法是更新到起始点的距离,Prim是更新到集合S的距离

acwing 858

2.1.1 朴素版 O ( n 2 ) O(n^2) O(n2)
#include<bits/stdc++.h>//O(n^2)
using namespace std;
const int N=510,INF=0x3f3f3f3f;
int g[N][N];
int dist[N];
bool st[N];//st为当前已经在连通块的点
int n,m;
int prim()
{
    memset(dist,0x3f,sizeof(dist));
    int res=0;//记录最小生成树的边权之和
    for(int i=0;i<n;i++)
    {
        int t=-1;
        for(int j=1;j<=n;j++)
        {
            if(!st[j]&&(t==-1||dist[t]>dist[j]))
                t=j;
        }
        if(i&&dist[t]==INF)return INF;
        if(i)res+=dist[t];//先累加以防下面自环时更新

        for(int j=1;j<=n;j++)
        {
            dist[j]=min(dist[j],g[t][j]);//更新到集合的距离
        }
        st[t]=true;
    }
    return res;
}
int main()
{
    memset(g,0x3f,sizeof(g));
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        g[a][b]=min(g[a][b],c);
        g[b][a]=min(g[b][a],c);
    }
    int res=prim();
    if(res==INF)cout<<"impossible"<<endl;
    else cout<<res<<endl;
    return 0;
}
2.1.2 堆优化 O ( m l o g n ) O(mlogn) O(mlogn)
//堆优化prim O(mlogn)
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;

const int MAXN = 510, MAXM = 2 * 1e5 + 10, INF = 0x3f3f3f3f;
typedef pair<int, int> PII;
int h[MAXM], e[MAXM], w[MAXM], ne[MAXM], idx;
bool vis[MAXN];
int n, m;

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

int Prim()
{
    memset(vis, false, sizeof vis);
    int sum = 0, cnt = 0;
    priority_queue<PII, vector<PII>, greater<PII>> q;
    q.push({0, 1});

    while (!q.empty())
    {
        auto t = q.top();
        q.pop();
        int ver = t.second, dst = t.first;
        if (vis[ver]) continue;
        vis[ver] = true, sum += dst, ++cnt;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (!vis[j]) {
                q.push({w[i], j});
            }
        }
    }

    if (cnt != n) return INF;
    return sum;
}

int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; ++i)
    {
        int a, b, w;
        cin >> a >> b >> w;
        add(a, b, w);
        add(b, a, w);
    }

    int t = Prim();
    if (t == INF) cout << "impossible" << endl;
    else cout << t << endl; 
}

2.2 kruskal O ( m l o g m ) O(mlogm) O(mlogm)

思路:将所有边按照权值的大小进行升序排序,然后从小到大一一判断。
如果这个边与之前选择的所有边不会组成回路,就选择这条边;反之,舍去。
直到具有 n 个顶点的连通网筛选出来 n-1 条边为止。
筛选出来的边和所有的顶点构成此连通网的最小生成树。

判断是否会产生回路的方法为:使用并查集。
在初始状态下给各个个顶点在不同的集合中。
遍历过程的每条边,判断这两个顶点的是否在一个集合中。
如果边上的这两个顶点在一个集合中,说明两个顶点已经连通,这条边不要。如果不在一个集合中,则要这条边。
acwing 859

#include<bits/stdc++.h>//基于并查集 O(mlogm)
using namespace std;
const int N=2*100010;
struct Edge
{
    int a,b,w;
}edges[N];
bool cmp(Edge a,Edge b)
{
    return a.w<b.w;
}
int n,m;
int f[N];
int find(int x)
{
    if(x!=f[x])f[x]=find(f[x]);
    else return f[x];
}
void kruskal()
{
    sort(edges,edges+m,cmp);
    int res=0,cnt=0;
    for(int i=0;i<m;i++)
    {
        int a=edges[i].a,b=edges[i].b,w=edges[i].w;
        if(find(a)!=find(b))//ab边不存在 ab是否在同一个连通块中
        {
            f[find(b)]=find(a);//连接ab边
            res+=w;
            cnt++;
        }
    }
    if(cnt<n-1)cout<<"impossible"<<endl;
    else cout<<res<<endl;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        f[i]=i;
    }
    for(int i=0;i<m;i++)
    {
        cin>>edges[i].a>>edges[i].b>>edges[i].w;
    }
    kruskal();
    return 0;
}

3.二分图

二分图:一定不含有奇数环,可能包含长度为偶数的环, 不一定是连通图
判断二分图充要条件:图中不含奇圈环

3.1 染色法 O ( n + m ) O(n+m) O(n+m)–判定二分图

染色法
将所有点分成两个集合,使得所有边只出现在集合之间,就是二分图
二分图:一定不含有奇数环,可能包含长度为偶数的环, 不一定是连通图
acwing 860

3.1.1 dfs版本

代码思路:
染色可以使用1和2区分不同颜色,用0表示未染色
遍历所有点,每次将未染色的点进行dfs, 默认染成1或者2
由于某个点染色成功不代表整个图就是二分图,因此只有某个点染色失败才能立刻break/return
染色失败相当于存在相邻的2个点染了相同的颜色

#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=2*N;
int h[N],e[M],ne[M],idx;
int color[N];
int n,m;
void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
bool dfs(int u,int c)
{
    color[u]=c;//染色
    for(int i=h[u];i!=-1;i=ne[i])
    {
        int j=e[i];
        if(!color[j])//未染色
        {
            if(!dfs(j,3-c))return false;//染别的颜色
        }
        if(color[j]==c)return false;//u的边与u染了相同颜色,染色矛盾,不是二分图
    }
    return true;
}
int main()
{
    memset(h,-1,sizeof(h));
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        int a,b;
        cin>>a>>b;
        add(a,b),add(b,a);
    }
    bool flag=true;
    for(int i=1;i<=n;i++)
    {
        if(!color[i])//未染色
        {
            if(!dfs(i,1))//染色失败
            {
                flag=false;
                break;
            }
        }
    }
    if(flag)cout<<"Yes"<<endl;
    else cout<<"No"<<endl;
    return 0;
}
3.1.2 bfs版本

代码思路
颜色 1 和 2 表示不同颜色, 0 表示 未染色
定义queue是存PII,表示 <点编号, 颜色>,
同理,遍历所有点, 将未染色的点都进行bfs
队列初始化将第i个点入队, 默认颜色可以是1或2
while (队列不空)
每次获取队头t, 并遍历队头t的所有邻边
若邻边的点未染色则染上与队头t相反的颜色,并添加到队列
若邻边的点已经染色且与队头t的颜色相同, 则返回false

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10;
typedef pair<int, int> PII;

int e[M], ne[M], h[N], idx;
int n, m;
int st[N];

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

bool bfs(int u){
    int hh = 0, tt = 0;
    PII q[N];
    q[0] = {u, 1};
    st[u] = 1;

    while(hh <= tt){
        auto t = q[hh ++];
        int ver = t.first, c = t.second;

        for (int i = h[ver]; i != -1; i = ne[i]){
            int j = e[i];

            if(!st[j])
            {
                st[j] = 3 - c;
                q[++ tt] = {j, 3 - c};
            }
            else if(st[j] == c) return false;
        }
    }

    return true;
}

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

    memset(h, -1, sizeof h);
    while(m --){
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a);
    }

    int flag = true;
    for(int i = 1; i <= n; i ++) {
        if (!st[i]){
            if(!bfs(i)){
                flag = false;
                break;
            }
        }
    }

    if (flag) puts("Yes");
    else puts("No");
    return 0;
}
例题

acwing 257
题目描述
S城现有两座监狱,一共关押着N名罪犯,编号分别为1−N。他们之间的关系自然也极不和谐。很多罪犯之间甚至积怨已久,如果客观条件具备则随时可能爆发冲突。我们用“怨气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度,怨气值越大,则这两名罪犯之间的积怨越多。如果两名怨气值为c的罪犯被关押在同一监狱,他们俩之间会发生摩擦,并造成影响力为c的冲突事件。
每年年末,警察局会将本年内监狱中的所有冲突事件按影响力从大到小排成一个列表,然后上报到S城Z市长那里。公务繁忙的Z市长只会去看列表中的第一个事件的影响力,如果影响很坏,他就会考虑撤换警察局长。
在详细考察了N名罪犯间的矛盾关系后,警察局长觉得压力巨大。他准备将罪犯们在两座监狱内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只要处于同一监狱内的某两个罪犯间有仇恨,那么他们一定会在每年的某个时候发生摩擦。
那么,应如何分配罪犯,才能使 Z 市长看到的那个冲突事件的影响力最小?这个最小值是多少?
输入格式
输入数据的每行中两个数之间用一个空格隔开。
第一行为两个正整数N和M,分别表示罪犯的数目以及存在仇恨的罪犯对数。
接下来的M行每行为三个正整数aj,bj,cj,表示aj号和bj号罪犯之间存在仇恨,其怨气值为cj
数据保证1<=aj<=bj<=N,0<=cj<=109,且每对罪犯组合只出现一次。
【数据范围】
对于30%的数据有N≤15。
对于70%的数据有N≤2000,M≤50000。
对于100%的数据有N≤20000,M≤100000。
输出格式
输出数据共1行,为Z市长看到的那个冲突事件的影响力。如果本年内监狱中未发生任何冲突事件,请输出0。

思路:将罪犯当做点,罪犯之间的仇恨关系当做点与点之间的无向边,边的权重是罪犯之间的仇恨值。
那么原问题变成:将所有点分成两组,使得各组内边的权重的最大值尽可能小。
我们在 [0,109]之间枚举最大边权 limit,当 limit 固定之后,剩下的问题就是:判断能否将所有点分成两组,使得所有权值大于 limit 的边都在组间,而不在组内。也就是判断由所有点以及所有权值大于 limit 的边构成的新图是否是二分图。
判断二分图用染色法,可以二分枚举limit

#include<bits/stdc++.h>
using namespace std;
const int N=20010,M=200010;
int n,m;
int h[N],e[M],w[M],ne[M],idx;
int color[N];
void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
bool dfs(int u,int c,int limit)
{
    color[u]=c;
    for(int i=h[u];i!=-1;i=ne[i])
    {
        if(w[i]<=limit) continue;//核心
        int j=e[i];
        if(color[j])
        {
            if(color[j]==c)return false;
        }
        else if(!dfs(j,3-c,limit))return false;
    }
    return true;
}
bool check(int limit)
{
    memset(color,0,sizeof(color));
    for(int i=1; i<=n; i++)
        if(color[i]==0)
            if(!dfs(i,1,limit))
                return false;
    return true;
}
int main()
{
    cin>>n>>m;

    memset(h,-1,sizeof(h));
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
        add(b,a,c);
    }

    int l = 0, r = 1e9;
    while (l<r)
    {
        int mid=l+r>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    }

    cout<<l<<endl;
    return 0;
}

3.2 匈牙利算法 O ( n m ) O(nm) O(nm),实际运行时间一般远小于O(nm)–二分图的最大匹配

补充概念:
匹配:在图论中,一个「匹配」是一个边的集合,其中任意两条边都没有公共顶点。
最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。
完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。
交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。
增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替 路称为增广路(agumenting path)。

最大匹配:给定一个二分图G,在G的一个子图M中, M的边集{E}中的任意两条边都不交汇于同一个结点,则称M是一个匹配,|M|最大的匹配称为最大匹配。
最小顶点覆盖:假如选了一个点就相当于覆盖了以它为端点的所有边,最小顶点覆盖就是选择最少的点来覆盖所有的边。
最大独立集:选出一些顶点使得这些顶点两两不相邻,则这些点构成的集合称为独立集。找出一个包含顶点数最多的独立集称为最大独立集。
(三者等价)

算法描述:
如果你想找的妹子已经有了男朋友,
你就去问问她男朋友,
你有没有备胎,
把这个让给我好吧
acwing 861

#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=2*N;
int h[N],e[M],ne[M],idx;
int match[N];//记录已有配对情况 match[j]=x表示左边的x配对右边的j
bool st[N];//记录右边是否配对 st[j]表示右边的j已经配对
int n1,n2,m;
void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
bool find(int x)
{
    for(int i=h[x];i!=-1;i=ne[i])//遍历x的所有边
    {
        int j=e[i];
        if(!st[j])//右边的j未被x预定匹配
        {
            st[j]=true;//x预定匹配j
            if(!match[j]||find(match[j]))//j没有匹配左边的对象或者j匹配的对象可以换一个不是j的匹配
            {
                match[j]=x;//x匹配j
                return true;
            }
        }
    }
    return false;
}
int main()
{
    memset(h,-1,sizeof(h));
    cin>>n1>>n2>>m;
    for(int i=0;i<m;i++)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);//只初始化从左到右的边
    }
    int res=0;
    for(int i=1;i<=n1;i++)//遍历左边所有顶点
    {
        memset(st,false,sizeof(st));//因为每次模拟匹配的预定情况都是不一样的所以每轮模拟都要初始化
        if(find(i))res++;//i匹配成功
    }
    cout<<res<<endl;
    return 0;
}
例题

落谷P2825
题目描述
在 2016 年,佳媛姐姐喜欢上了一款游戏,叫做泡泡堂。
简单的说,这个游戏就是在一张地图上放上若干个炸弹,看是否能炸到对手,或者躲开对手的炸弹。在玩游戏的过程中,小 H 想到了这样一个问题:当给定一张地图,在这张地图上最多能放上多少个炸弹能使得任意两个炸弹之间不会互相炸到。炸弹能炸到的范围是该炸弹所在的一行和一列,炸弹的威力可以穿透软石头,但是不能穿透硬石头。
给定一张 n×m 的网格地图:其中 * 代表空地,炸弹的威力可以穿透,可以在空地上放置一枚炸弹。x 代表软石头,炸弹的威力可以穿透,不能在此放置炸弹。# 代表硬石头,炸弹的威力是不能穿透的,不能在此放置炸弹。例如:给出 1×4 的网格地图 *xx*,这个地图上最多只能放置一个炸弹。给出另一个 1×4 的网格地图 *x#*,这个地图最多能放置两个炸弹。
现在小 H 任意给出一张 n×m 的网格地图,问你最多能放置多少炸弹。
输入格式
第一行输入两个正整数 n, m,n表示地图的行数,m表示地图的列数。
接下来输入 n行 m列个字符,代表网格地图。*的个数不超过 n×m 个。
输出格式
输出一个整数 a,表示最多能放置炸弹的个数。

思路
二分图匹配
如果没有硬石头,那么只需将行看作是二分图的一边,列看作另一边,然后可以放置炸弹的地方,所在行向所在列连边即可。
本来一行只能放一颗炸弹,但硬石头的出现导致一行被隔开,所以在有硬石头的情况下,可以把一行看成多行。
即把行和列依据硬石头的位置分割成多行和多列
如题中示例

#***
*#**
**#*
xxx#

每个格子所在的行为:

#111
2#22
33#3
444#

考虑到硬石头,则每个格子所在的"行"改为:

#111
2#33
44#5
666#

即原来第1行被硬石头分割成了第1行
原来第2行被硬石头分割成了第2行和第3行
原来第3行被硬石头分割成了第4行和第5行
原来第4行被硬石头分割成了第6行
#(硬石头)所在的格子不属于任一行
对列同样操作
初始每个格子所在列:

#234
1#34
12#4
122#

重新编号后每个格子所在列:

#246
1#46
13#6
135#

即原来第1列被硬石头分割成了第1列
原来第2列被硬石头分割成了第2列和第3列
原来第3列被硬石头分割成了第4列和第5列
原来第4列被硬石头分割成了第6列
#(硬石头)所在的格子不属于任一列

#include<bits/stdc++.h>
using namespace std;
const int N = 60,M=100010;
int h[N], e[M], ne[M], idx;
int n, m;
char g[N][N];
int row[N][N], col[N][N];
bool st[N * N];
int match[N * N];
void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool find(int x) 
{
	for (int i = h[x]; i != -1; i = ne[i])
	{
		int j = e[i];
		if (!st[j])
		{
			st[j] = 1;
			if (!match[j] || find(match[j]))
			{
				match[j] = x;
				return 1;
			}
		}
	}
	return 0;
}

int main() 
{
	memset(h, -1, sizeof(h));
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		cin >> g[i] + 1;
	int num = 0;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= m; j++) 
		{
			if (g[i][j] == '#') continue;
			if (j == 1 || g[i][j - 1] == '#') num++;
			row[i][j] = num;
		}
	}
	for (int j = 1; j <= m; j++)
	{
		for (int i = 1; i <= n; i++)
		{
			if (g[i][j] == '#') continue;
			if (i == 1 || g[i - 1][j] == '#') num++;
			col[i][j] = num;
		}
	}
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++)
			if (g[i][j] == '*')
				add(row[i][j],col[i][j]);
	int ans = 0;
	for (int i = 1; i <= num; i++) 
	{
		memset(st, 0, sizeof(st));
		if (find(i)) ans++;
	}
	cout << ans << endl;
	return 0;
}

4.网络流

4.1 最大流问题

4.1.1 Edmonds-Karp算法 O ( n m 2 ) O(nm^2) O(nm2)

思路:
邻接表存残存网络 G f G_f Gf
增广路 p p p的残存容量 f p = m i n { c f ( u , v ) : u , v 属 于 路 径 p } f_p=min\{c_f(u,v):u,v属于路径p\} fp=min{cf(u,v):u,vp}
while(找到增广路 p p p)
{
 最大流+=增广路 p p p的残存容量 f p f_p fp
 修改增广路每条边的容量(正向边+= f p f_p fp,反向边-= f p f_p fp)
}
acwing 2171

#include<bits/stdc++.h>
using namespace std;
const int N=1010,M=20010,INF=1e8;
int h[N],e[M],ne[M],f[M],idx;
void add(int a,int b,int c)
{
    e[idx]=b,f[idx]=c,ne[idx]=h[a],h[a]=idx++;
    e[idx]=a,f[idx]=0,ne[idx]=h[b],h[b]=idx++;
}
int q[N],d[N],pre[N];
bool st[N];
int n,m,s,t;
bool bfs()
{
    memset(st,false,sizeof(st));
    int hh=0,tt=0;
    q[0]=s;
    st[s]=true;
    d[s]=INF;
    while(hh<=tt)
    {
        int k=q[hh++];
        for(int i=h[k];~i;i=ne[i])
        {
            int ver=e[i];
            if(!st[ver]&&f[i])
            {
                st[ver]=true;
                d[ver]=min(d[k],f[i]);
                pre[ver]=i;
                if(ver==t)return true;
                q[++tt]=ver;
            }
        }
    }
    return false;
}
int EK()
{
    int r=0;
    while(bfs())
    {
        r+=d[t];
        for(int i=t;i!=s;i=e[pre[i]^1])
        {
            f[pre[i]]-=d[t];
            f[pre[i]^1]+=d[t];
        }
    }
    return r;
}
int main()
{
    cin>>n>>m>>s>>t;
    memset(h,-1,sizeof(h));
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    cout<<EK()<<endl;
    return 0;
}
//其它实现方法
#include<bits/stdc++.h>
using namespace std;
const int maxn = 110, INF = 0x3f3f3f3f;
struct Edge {
	int from, to, cap, flow;
	Edge(int u, int v, int c, int f) :from(u), to(v), cap(c), flow(f) {}
};
struct EdmondsKarp {
	int n, m;
	vector<Edge> edges; //边数的两倍
	vector<int> G[maxn]; //邻接表,G[i][j]表示结点i的第j条边在e数组中的序号
	int a[maxn]; //当起点到i的可改进量
	int p[maxn]; //最短路树上p的入弧编号
	void init(int n) {
		for (int i = 0; i < n; i++) G[i].clear();
		edges.clear();
	}
	void AddEdge(int from, int to, int cap) {
		edges.push_back(Edge(from, to, cap, 0));
		edges.push_back(Edge(to, from, 0, 0)); //反向弧
		m = edges.size();
		G[from].push_back(m - 2);
		G[to].push_back(m - 1);
	}
	long long Maxflow(int s, int t) {
		long long flow = 0;
		for (;;) {
			memset(a, 0, sizeof(a));
			queue<int> Q;
			Q.push(s);
			a[s] = INF;
			while (!Q.empty()) {
				int x = Q.front(); Q.pop();
				for (int i = 0; i < G[x].size(); i++) {
					Edge& e = edges[G[x][i]];
					if (!a[e.to] && e.cap > e.flow) {
						p[e.to] = G[x][i];
						a[e.to] = min(a[x], e.cap - e.flow);
						Q.push(e.to);
					}
				}
				if (a[t]) break;
			}
			if (!a[t]) break;
			for (int u = t; u != s; u = edges[p[u]].from) {
				edges[p[u]].flow += a[t];
				edges[p[u] ^ 1].flow -= a[t];
			}
			flow += a[t];
		}
		return flow;
	}
};
int main()
{
	int n, m, s, t;
	cin >> n >> m >> s >> t;
	EdmondsKarp e;
	e.init(n);
	for (int i = 0; i < m; i++)
	{
		int u, v, c;
		cin >> u >> v >> c;
		e.AddEdge(u, v, c);
	}
	cout << e.Maxflow(s, t) << endl;
}
4.1.2 Dinic算法 O ( m n 2 ) O(mn^2) O(mn2)

Dinic算法框架
在残量网络上 B F S BFS BFS求出节点的层次,构造分层图
在分层图上 D F S DFS DFS寻找增广路,在回溯时同时更新边权

当前弧优化
对于一个节点 x x x,当它在 D F S DFS DFS中走到了第 i i i条弧时,前 i − 1 i-1 i1条弧到汇点的流一定已经被流满而没有可行的路线了

那么当下一次再访问 x x x节点时,前 i − 1 i-1 i1条弧就没有任何意义了

所以我们可以在每次枚举节点 x x x所连的弧时,改变枚举的起点,这样就可以删除起点以前的所有弧,来达到优化剪枝的效果

对应到代码中,就是 c u r cur cur数组
acwing 2172

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 10010, M = 200010, INF = 1e8;

int n, m, S, T;
int h[N], e[M], f[M], ne[M], idx;
int q[N], d[N], cur[N];

void add(int a, int b, int c)
{
    e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
    e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}

bool bfs()
{
    int hh = 0, tt = 0;
    memset(d, -1, sizeof d);
    q[0] = S, d[S] = 0, cur[S] = h[S];
    while (hh <= tt)
    {
        int t = q[hh ++ ];
        for (int i = h[t]; ~i; i = ne[i])
        {
            int ver = e[i];
            if (d[ver] == -1 && f[i])
            {
                d[ver] = d[t] + 1;
                cur[ver] = h[ver];
                if (ver == T)  return true;
                q[ ++ tt] = ver;
            }
        }
    }
    return false;
}

int find(int u, int limit)
{
    if (u == T) return limit;
    int flow = 0;
    for (int i = cur[u]; ~i && flow < limit; i = ne[i])
    {
        cur[u] = i;  // 当前弧优化
        int ver = e[i];
        if (d[ver] == d[u] + 1 && f[i])
        {
            int t = find(ver, min(f[i], limit - flow));
            if (!t) d[ver] = -1;
            f[i] -= t, f[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

int dinic()
{
    int r = 0, flow;
    while (bfs()) while (flow = find(S, INF)) r += flow;
    return r;
}

int main()
{
    scanf("%d%d%d%d", &n, &m, &S, &T);
    memset(h, -1, sizeof h);
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    printf("%d\n", dinic());

    return 0;
}

4.2 最小费用最大流问题

acwing 2174

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 5010, M = 100010, INF = 1e8;

int n, m, S, T;
int h[N], e[M], f[M], w[M], ne[M], idx;
int q[N], d[N], pre[N], incf[N];
bool st[N];

void add(int a, int b, int c, int d)
{
    e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++ ;
    e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++ ;
}

bool spfa()
{
    int hh = 0, tt = 1;
    memset(d, 0x3f, sizeof d);
    memset(incf, 0, sizeof incf);
    q[0] = S, d[S] = 0, incf[S] = INF;
    while (hh != tt)
    {
        int t = q[hh ++ ];
        if (hh == N) hh = 0;
        st[t] = false;

        for (int i = h[t]; ~i; i = ne[i])
        {
            int ver = e[i];
            if (f[i] && d[ver] > d[t] + w[i])
            {
                d[ver] = d[t] + w[i];
                pre[ver] = i;
                incf[ver] = min(f[i], incf[t]);
                if (!st[ver])
                {
                    q[tt ++ ] = ver;
                    if (tt == N) tt = 0;
                    st[ver] = true;
                }
            }
        }
    }

    return incf[T] > 0;
}

void EK(int& flow, int& cost)
{
    flow = cost = 0;
    while (spfa())
    {
        int t = incf[T];
        flow += t, cost += t * d[T];
        for (int i = T; i != S; i = e[pre[i] ^ 1])
        {
            f[pre[i]] -= t;
            f[pre[i] ^ 1] += t;
        }
    }
}

int main()
{
    scanf("%d%d%d%d", &n, &m, &S, &T);
    memset(h, -1, sizeof h);
    while (m -- )
    {
        int a, b, c, d;
        scanf("%d%d%d%d", &a, &b, &c, &d);
        add(a, b, c, d);
    }

    int flow, cost;
    EK(flow, cost);
    printf("%d %d\n", flow, cost);

    return 0;
}

五、动态规划

1.背包问题

1.1 01背包问题

acwing 2
介绍:背包容量为 m m m,有 n n n件物品,第 i i i件物品体积为 v i v_i vi,价值为 w i w_i wi,每件物品只能选一次,求最大价值
dp思想:
1.状态表示: f [ i , j ] f[i,j] f[i,j]表示从前i个物品中选且总体积小于j的最大值
2.状态计算:按是否选第 i i i个物品划分
选第 i i i个物品: f [ i , j ] = f [ i − 1 , j − v i ] + w i f[i,j]=f[i-1,j-v_i]+w_i f[i,j]=f[i1,jvi]+wi
不选第 i i i个物品: f [ i , j ] = f [ i − 1 , j ] f[i,j]=f[i-1,j] f[i,j]=f[i1,j]
f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v i ] + w i ) f[i,j]=max(f[i-1,j],f[i-1,j-v_i]+w_i) f[i,j]=max(f[i1,j],f[i1,jvi]+wi)

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int d[N][N];//d[i][j]指的是只从前i个物品中选且总体积小于j的最大价值
int f[N];
int v[N];
int w[N];
int main()
{
    int n,c;
    cin>>n>>c;
    for(int i=1;i<=n;i++)
    {
        cin>>v[i]>>w[i];
    }
    for(int i=1;i<=n;i++)//二维
    {
        for(int j=0;j<=c;j++)
        {
            d[i][j]=d[i-1][j];//j<v[i]的情况,此时d[i-1][j-v[i]]为0
            if(j>=v[i])d[i][j]=max(d[i-1][j],d[i-1][j-v[i]]+w[i]);//含i和不含i两类
        }
    }
    for(int i=1;i<=n;i++)//一维
    {
        for(int j=c;j>=v[i];j--)
        {
            f[j]=max(f[j],f[j-v[i]]+w[i]);//删掉了一维,且j从0到v[i]-1无意义,j循环从v[i]开始递增,又由于f[j-v[i]]一定比f[j]先算出来,而f[j-v[i]]也是在第i层被计算的(f[j-v[i]]可能在第i层被f[j]覆盖),等价于用f[i][j-v[i]]来更新,故要从后往前遍历
        }
    }
    cout<<f[c]<<endl;
    //cout<<d[n][c]<<endl;
}

1.2 完全背包问题

acwing 3
介绍:背包容量为 m m m,有 n n n种物品,每种物品可以任意选择个数,第 i i i种物品体积为 v i v_i vi,价值为 w i w_i wi,求最大价值
dp思想:
1.状态表示: f [ i , j ] f[i,j] f[i,j]表示从前i个物品中选且总体积小于j的最大值
2.状态计算:按选k个第i个物品划分 ( 0 ≤ k ≤ [ j v i ] , k ∈ Z ) (0≤k≤[\frac{j}{v_i}],k \in Z) (0k[vij],kZ)
f [ i , j ] = m a x ( f [ i , j ] , f [ i − 1 , j − k v i ] + k w i ) f[i,j]=max(f[i,j],f[i-1,j-kv_i]+kw_i) f[i,j]=max(f[i,j],f[i1,jkvi]+kwi)
优化:
f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v i ] + w i , f [ i − 1 , j − 2 v i ] + 2 w i , f [ i − 1 , j − 3 v i ] + 3 w i , ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ) f[i,j]=max(f[i-1,j],f[i-1,j-v_i]+w_i,f[i-1,j-2v_i]+2w_i,f[i-1,j-3v_i]+3w_i,······) f[i,j]=max(f[i1,j],f[i1,jvi]+wi,f[i1,j2vi]+2wi,f[i1,j3vi]+3wi,),有 [ j v i ] + 1 [\frac{j}{v_i}]+1 [vij]+1
f [ i , j − v i ] = m a x ( f [ i − 1 , j − v i ] , f [ i − 1 , j − 2 v i ] + w i , f [ i − 1 , j − 3 v i ] + 2 w i , ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ) f[i,j-v_i]=max(f[i-1,j-v_i],f[i-1,j-2v_i]+w_i,f[i-1,j-3v_i]+2w_i,······) f[i,jvi]=max(f[i1,jvi],f[i1,j2vi]+wi,f[i1,j3vi]+2wi,),有 [ j v i ] [\frac{j}{v_i}] [vij]
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − v [ i ] ] + w [ i ] ) f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]) f[i][j]=max(f[i1][j],f[i][jv[i]]+w[i])

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int d[N][N];//d[i][j]表示只从前i个物品中选且总体积小于j的最大价值
int f[N][N];
int dp[N];
int v[N],w[N];
int n,m;
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
    //for(int i=1;i<=n;i++)//一般解法,可能tle
    //{
    //    for(int j=0;j<=m;j++)
    //    {
    //        for(int k=0;k*v[i]<=j;k++)
    //        {
    //            d[i][j]=max(d[i][j],d[i-1][j-k*v[i]]+w[i]*k);
    //        }
    //    }
    //}

    //f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i],f[i-1][j-2*v[i]]+2*w[i],f[i-1][j-3*v[i]]+3*w[i],······) j/v[i]+1项
    //f[i][j-v[i]]=max(     f[i-1][j-v[i]],     f[i-1][j-2*v[i]]+*w[i],f[i-1][j-3*v[i]]+2*w[i],······)  j/v[i]项
    //故f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]);
    //for(int i=1;i<=n;i++)//优化
    //{
    //    for(int j=0;j<=m;j++)
    //    {
    //        f[i][j]=f[i-1][j];
    //        if(j>=v[i])f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
    //    }
    //}
    for(int i=1;i<=n;i++)//再优化
    {
        for(int j=v[i];j<=m;j++)
        {
            dp[j]=max(dp[j],dp[j-v[i]]+w[i]);//dp[j-v[i]]一定是第i层算的,故j直接从小到大枚举
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}

1.3 多重背包问题

介绍:背包容量为 m m m,有 n n n种物品,第 i i i种物品最多可以任意选择 s i s_i si件,每件体积为 v i v_i vi,价值为 w i w_i wi,求最大价值

1.3.1 暴力dp

acwing 4
dp思想:
暴力枚举第i种物品选几个
1.状态表示: f [ i , j ] f[i,j] f[i,j]表示从前i个物品中选且总体积小于j的最大值
2.状态计算:按选k个第i个物品划分 ( 0 ≤ k ≤ [ j v i ] , k ≤ s i , k ∈ Z ) (0≤k≤[\frac{j}{v_i}],k≤s_i,k \in Z) (0k[vij],ksi,kZ) f [ i , j ] = m a x ( f [ i , j ] , f [ i − 1 , j − k v i ] + k w i ) f[i,j]=max(f[i,j],f[i-1,j-kv_i]+kw_i) f[i,j]=max(f[i,j],f[i1,jkvi]+kwi)

#include<bits/stdc++.h>//暴力枚举第i种物品选几个
using namespace std;
const int N=210;
int d[N][N];
int v[N],s[N],w[N];
int n,m;
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>v[i]>>w[i]>>s[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=m;j++)
        {
            for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
            {
                d[i][j]=max(d[i][j],d[i-1][j-k*v[i]]+k*w[i]);
            }
        }
    }
    cout<<d[n][m]<<endl;
    return 0;
}
1.3.2 二进制优化

acwing 5
f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w , f [ i − 1 , j − 2 v ] + 2 w , . . . . . f [ i − 1 , j − s v ] + s w ) f[i , j ] = max( f[i-1,j] ,f[i-1,j-v]+w ,f[i-1,j-2v]+2w ,..... f[i-1,j-sv]+sw) f[i,j]=max(f[i1,j],f[i1,jv]+w,f[i1,j2v]+2w,.....f[i1,jsv]+sw),有 s s s
f [ i , j − v ] = m a x ( f [ i − 1 , j − v ] , f [ i − 1 , j − 2 v ] + w , . . . . . f [ i − 1 , j − s v ] + ( s − 1 ) w , f [ i − 1 , j − ( s + 1 ) v ] + s w ) f[i , j-v]= max( f[i-1,j-v] ,f[i-1,j-2v]+w, ..... f[i-1,j-sv]+(s-1)w, f[i-1,j-(s+1)v]+sw ) f[i,jv]=max(f[i1,jv],f[i1,j2v]+w,.....f[i1,jsv]+(s1)w,f[i1,j(s+1)v]+sw),有 s s s
多了一项 f [ i − 1 , j − ( s + 1 ) v ] + s w f[i-1,j-(s+1)v]+sw f[i1,j(s+1)v]+sw,不能像完全背包问题那样优化

s s s拆成 1 , 2 , 4 , 8 , ⋅ ⋅ ⋅ , 2 k , c 1,2,4,8,···,2^k ,c 1,2,4,8,2k,c若干份,总和为 s s s c < 2 k + 1 c<2^{k+1} c<2k+1,每一份分为取与不取可以表示 [ 0 , s ] [0,s] [0,s]中的任何数,用 ⌈ l o g s ⌉ ⌈logs⌉ logs个新的物品表示出第i个物品,转为01背包问题

#include<bits/stdc++.h>
using namespace std;
const int N=25010,M=2010;
int d[N];
int v[N],w[N],s[M];
int n,m;
int main()
{
    cin>>n>>m;
    int cnt=0;
    for(int i=1;i<=n;i++)
    {
        int a,b,s;
        cin>>a>>b>>s;
        int k=1;
        while(k<=s)
        {
            cnt++;
            v[cnt]=a*k;
            w[cnt]=b*k;
            s-=k;
            k*=2;
        }
        if(s>0)
        {
            cnt++;
            v[cnt]=a*s;
            w[cnt]=b*s;
        }

    }
    int n=cnt;
    for(int i=1;i<=n;i++)
        for(int j=m;j>=v[i];j--)
            d[j]=max(d[j],d[j-v[i]]+w[i]);
    cout<<d[m]<<endl;
    return 0;
}

1.4 分组背包问题

acwing 9
介绍:背包容量为 m m m,有 n n n组物品,每组物品有 s i s_i si个,同一组内的物品最多只能选一个。每件物品的体积是 v i j v_{ij} vij,价值是 w i j w_{ij} wij,其中 i i i是组号, j j j是组内编号。求最大价值
dp思想:
1.状态表示: f [ i , j ] f[i,j] f[i,j]表示从前i个物品中选且总体积小于j的最大值
2.状态计算:按是否选第i组的物品划分
不选: f [ i , j ] = f [ i − 1 , j ] f[i,j]=f[i-1,j] f[i,j]=f[i1,j]
选:按选第 i i i组的第 k k k个划分 ( 0 ≤ k ≤ s i , k ∈ Z ) (0≤k≤s_i,k \in Z) (0ksi,kZ)
f [ i , j ] = m a x ( f [ i , j ] , f [ i − 1 , j − v i k ] + w i k ) f[i,j]=max(f[i,j],f[i-1,j-v_{ik}]+w_{ik}) f[i,j]=max(f[i,j],f[i1,jvik]+wik)

#include<bits/stdc++.h>
using namespace std;
const int N=110;
int v[N][N],w[N][N];
int d[N][N],s[N];
int n,m;
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>s[i];
        for(int j=1;j<=s[i];j++)
            cin>>v[i][j]>>w[i][j];
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=m;j++)
        {
            d[i][j]=d[i-1][j];//不选
            for(int k=1;k<=s[i];k++)
                if(j>=v[i][k])
                    d[i][j]=max(d[i][j],d[i-1][j-v[i][k]]+w[i][k]);
        }
    }
    cout<<d[n][m]<<endl;
    return 0;
}
//因为只用到了第i-1列,所以可以仿照01背包的套路逆向枚举体积
#include<bits/stdc++.h>
using namespace std;

const int N=110;
int f[N];
int v[N][N],w[N][N],s[N];
int n,m,k;

int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        cin>>s[i];
        for(int j=0;j<s[i];j++)
        {
            cin>>v[i][j]>>w[i][j];
        }
    }

    for(int i=0;i<n;i++)
    {
        for(int j=m;j>=0;j--)
        {
            for(int k=0;k<s[i];k++)//for(int k=s[i];k>=1;k--)也可以
            {    
                if(j>=v[i][k])     f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);  
            }
        }
    }
    cout<<f[m]<<endl;
}

2.线性dp

2.1 数字三角形

acwing 898
dp思想:
1.状态表示: f [ i , j ] f[i,j] f[i,j]表示走到坐标为 ( i , j ) (i,j) (i,j)的最大路径
2.状态计算:划分为从 ( i − 1 , j ) (i-1,j) (i1,j)还是 ( i − 1 , j − 1 ) (i-1,j-1) (i1,j1) ( i , j ) (i,j) (i,j)
f [ i , j ] = m a x ( f [ i − 1 , i − 1 ] + a [ i , j ] , f [ i − 1 , j ] + a [ i , j ] ) f[i,j]=max(f[i-1,i-1]+a[i,j],f[i-1,j]+a[i,j]) f[i,j]=max(f[i1,i1]+a[i,j],f[i1,j]+a[i,j])

#include<bits/stdc++.h>
using namespace std;
const int N=510,INF=1e9;
int d[N][N];
int a[N][N];
int n;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=i;j++)
        {
            cin>>a[i][j];
        }
    }
    for(int i=0;i<=n;i++)
    {
        for(int j=0;j<=i+1;j++)
        {
            d[i][j]=-INF;//要给d初始化,不然会走数组外的数
        }
    }
    d[1][1]=a[1][1];
    for(int i=2;i<=n;i++)
    {
        for(int j=1;j<=i;j++)
        {
            d[i][j]=max(d[i-1][j-1]+a[i][j],d[i-1][j]+a[i][j]);
        }
    }
    int ans=-INF;
    for(int i=1;i<=n;i++)
    {
        ans=max(ans,d[n][i]);
    }
    cout<<ans<<endl;
    return 0;
}

2.2 最长上升子序列

2.2.1 暴力dp O ( n 2 ) O(n^2) O(n2)

acwing 895
dp思想:
1.状态表示: f [ i ] f[i] f[i]表示的是从第一个数字开始算,以 a [ i ] a[i] a[i]结尾的最大的上升序列
2.状态计算:按以 a [ i ] a[i] a[i]结尾的最大的上升序列的前一个数 a [ j ] a[j] a[j]的位置划分 j ∈ ( 0 , 1 , 2 , . . , i − 1 ) j∈(0,1,2,..,i-1) j(0,1,2,..,i1), 在 a [ i ] > a [ j ] a[i] > a[j] a[i]>a[j]时, f [ i ] = m a x ( f [ i ] , f [ j ] + 1 ) f[i] = max(f[i], f[j] + 1) f[i]=max(f[i],f[j]+1)
有一个边界,若前面没有比i小的, f [ i ] f[i] f[i]为1(自己为结尾)。
最后在找 f [ i ] f[i] f[i]的最大值。

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int n;
int a[N],d[N];//d[i]指的是从第一个数字开始算,以a[i]结尾的最大的上升序列
int g[N];
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++)
    {
        d[i]=1;
        g[i]=0;
        for(int j=1;j<i;j++)
        {
            if(a[j]<a[i])
            {
                if(d[i]<d[j]+1)
                {
                    d[i]=d[j]+1;
                    g[i]=j;//最大上升子序列前一个数的下标
                }
            }
        }
    }
    int k=1;
    for(int i=1;i<=n;i++)if(d[k]<d[i])k=i;
    cout<<d[k]<<endl;
    for(int i=0,len=d[k];i<len;i++)
    {
        cout<<a[k]<<" ";
        k=g[k];
    }
    return 0;
}
2.2.2 dp+二分 O ( n l o g n ) O(nlogn) O(nlogn)

acwing 896
dp思想:
1.状态表示: q [ i ] q[i] q[i]表示长度为 i i i的最长上升子序列,末尾最小的数字。(长度为 i i i的最长上升子序列所有结尾中,结尾最小 m i n min min的) 即长度为 i i i的子序列末尾最小元素是什么。
2.状态计算:对于每一个 a [ i ] a[i] a[i] l e n len len记录最长上升子序列的长度,二分找到q数组中最大的小于当前数 a [ i ] a[i] a[i]的数的下标 r r r l e n = m a x ( l e n , r + 1 ) , q [ r + 1 ] = a [ i ] len=max(len,r+1),q[r+1]=a[i] len=max(len,r+1)q[r+1]=a[i]
q [ i ] q[i] q[i]一定是一个单调递增的数组

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n;
int a[N];
int q[N];//存的是以长度为i的上升子序列中末尾元素最小的数
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    int len=0;
    for(int i=1;i<=n;i++)//二分找到q数组中最大的小于当前数a[i]的数的下标r
    {
        int l=0,r=len;
        while(l<r)
        {
            int mid=l+r+1>>1;
            if(q[mid]<a[i])l=mid;
            else r=mid-1;
        }
        len=max(len,r+1);
        q[r+1]=a[i];//将a[i]赋值给q[r+1]
    }
    cout<<len<<endl;

    return 0;
}

2.3 最长公共子序列

acwing 897
dp思想:
1.状态表示: f [ i , j ] f[i,j] f[i,j]表示a的前i个字母,和b的前j个字母的最长公共子序列长度
2.状态计算:以 a [ i ] , b [ j ] a[i],b[j] a[i],b[j]是否包含在子序列当中为依据,因此可以分成四类:
(1) a [ i ] a[i] a[i]不在, b [ j ] b[j] b[j]不在
m a x = f [ i − 1 , j − 1 ] max=f[i−1,j−1] max=f[i1,j1]
(2) a [ i ] a[i] a[i]不在, b [ j ] b[j] b[j]
看似是 m a x = f [ i − 1 , j ] max=f[i−1,j] max=f[i1,j], 实际上无法用 f [ i − 1 , j ] f[i−1,j] f[i1,j]表示,因为 f [ i − 1 , j ] f[i−1,j] f[i1,j]表示的是在 a a a的前 i − 1 i-1 i1个字母中出现,并且在 b b b的前 j j j个字母中出现,此时 b [ j ] b[j] b[j]不一定出现,这与条件不完全相等,条件给定是 a [ i ] a[i] a[i]一定不在子序列中, b [ j ] b[j] b[j]一定在子序列当中,但仍可以用 f [ i − 1 , j ] f[i−1,j] f[i1,j]来表示,原因就在于条件给定的情况被包含在 f [ i − 1 , j ] f[i−1,j] f[i1,j]中,即条件的情况是 f [ i − 1 , j ] f[i−1,j] f[i1,j]的子集,而求的是 m a x max max,所以对结果不影响。
例如:要求 a , b , c a,b,c abc的最大值可以这样求: m a x ( m a x ( a , b ) , m a x ( b , c ) ) m a x ( m a x ( a , b ) , m a x ( b , c ) ) max(max(a,b),max(b,c))max(max(a,b),max(b,c)) max(max(a,b),max(b,c))max(max(a,b),max(b,c))虽然 b b b被重复使用,但仍能求出 m a x max max,求 m a x max max只要保证不漏即可。
(3) a [ i ] a[i] a[i]在, b [ j ] b[j] b[j]不在 m a x = f [ i , j − 1 ] max=f[i,j-1] max=f[i,j1]原理同(2)
(4) a [ i ] a[i] a[i]在, b [ j ] b[j] b[j] m a x = f [ i − 1 , j − 1 ] + 1 max=f[i−1,j−1]+1 max=f[i1,j1]+1;
实际上,在计算时,(1)包含在(2)和(3)的情况中,所以(1)不用考虑

也可以按 a [ i ] a[i] a[i]是否等于 b [ j ] b[j] b[j]来划分

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int f[N][N];
char a[N],b[N];
int main()
{
    int n,m;
    cin>>n>>m;
    cin>>a+1>>b+1;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            f[i][j]=max(f[i-1][j],f[i][j-1]);
            if(a[i]==b[j])f[i][j]=max(f[i][j],f[i-1][j-1]+1);
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

3.区间dp

acwing 282
dp思想:
1.状态表示: f [ i , j ] f[i,j] f[i,j]表示 [ i , j ] [i,j] [i,j]区间石子合并的最小代价
2.状态计算:区间为 [ i , k ] [i,k] [i,k] [ k + 1 , j ] [k+1,j] [k+1,j],按 k k k在哪划分 ( i ≤ k ≤ j − 1 ) (i≤k≤j-1) (ikj1)
f [ i , j ] = m i n ( f [ i , j ] , f [ i , k ] + f [ k + 1 , j ] + s [ j ] − s [ i − 1 ] ) f[i,j]=min(f[i,j],f[i,k]+f[k+1,j]+s[j]-s[i-1]) f[i,j]=min(f[i,j],f[i,k]+f[k+1,j]+s[j]s[i1])

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int s[N];
int f[N][N];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)cin>>s[i];
    for(int i=1;i<=n;i++)s[i]+=s[i-1];
    for(int len=2;len<=n;len++)//len=1时无代价,故len从2开始枚举
    {
        for(int i=1;i+len-1<=n;i++)
        {
            int l=i,r=i+len-1;
            f[l][r]=1e9;
            for(int k=l;k<r;k++)
            {
                f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[i-1]);
            }
        }
    }
    cout<<f[1][n]<<endl;
    return 0;
}

4.计数类dp

acwing 900

4.1 解法1:完全背包问题变形

可以转化为完全背包问题:
背包容量为 n n n,有 n n n种物品,每种物品可以任意选择个数,第 i i i种物品体积为 i i i,体积恰好为 n n n的方案数量
dp思想:
1.状态表示: f [ i , j ] f[i,j] f[i,j]表示从前 i i i个选,体积恰好为 j j j的方案数量
2.状态计算:按选 k k k个第 i i i个物品划分 ( 0 ≤ k ≤ [ j i ] , k ∈ Z ) (0≤k≤[\frac{j}{i}],k \in Z) (0k[ij],kZ)
f [ i , j ] = f [ i , j ] + f [ i − 1 , j − k i ] f[i,j]=f[i,j]+f[i-1,j-ki] f[i,j]=f[i,j]+f[i1,jki]
优化:
f [ i , j ] = f [ i − 1 , j ] + f [ i − 1 , j − i ] + f [ i − 1 , j − 2 i ] ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ f[i,j]=f[i-1,j]+f[i-1,j-i]+f[i-1,j-2i]······ f[i,j]=f[i1,j]+f[i1,ji]+f[i1,j2i],有 [ j i ] + 1 [\frac{j}{i}]+1 [ij]+1
f [ i , j − i ] = f [ i − 1 , j − i ] + f [ i − 1 , j − 2 i ] + f [ i − 1 , j − 3 i ] ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ f[i,j-i]=f[i-1,j-i]+f[i-1,j-2i]+f[i-1,j-3i]······ f[i,ji]=f[i1,ji]+f[i1,j2i]+f[i1,j3i],有 [ j i ] [\frac{j}{i}] [ij]
f [ i , j ] = f [ i − 1 , j ] + f [ i , j − i ] f[i,j]=f[i-1,j]+f[i,j-i] f[i,j]=f[i1,j]+f[i,ji]

//朴素
#include<bits/stdc++.h>
using namespace std;
const int N=1010,mod=1e9+7;
int f[N][N];//f[i,j]表示从前i个选,体积恰好为j的方案数
int main()
{
    int n;
    cin>>n;
    f[0][0]=1;//一个数都不选
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=n;j++)
        {
            for(int k=0;k*i<=j;k++)
            {
                f[i][j]=f[i][j]+f[i-1][j-k*i];
                f[i][j]%=mod;
            }
        }
    }
    cout<<f[n][n]<<endl;
    return 0;
}
//优化
//二维
#include<bits/stdc++.h>
using namespace std;
const int N=1010,mod=1e9+7;
int f[N][N];//f[i,j]表示从前i个选,体积恰好为j的方案数
int main()
{
    int n;
    cin>>n;
    f[0][0]=1;//一个数都不选时为1
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=n;j++)
        {
            f[i][j] = f[i - 1][j] % mod; 
            if (j >= i) f[i][j]=(f[i-1][j]+f[i][j-i])%mod;
        }
    }
    cout<<f[n][n]<<endl;
    return 0;
}
//一维
#include<bits/stdc++.h>
using namespace std;
const int N=1010,mod=1e9+7;
int f[N];//f[i,j]表示从前i个选,体积恰好为j的方案数
int main()
{
    int n;
    cin>>n;
    f[0]=1;//一个数都不选为1
    for(int i=1;i<=n;i++)
    {
        for(int j=i;j<=n;j++)
        {
            f[j]=(f[j]+f[j-i])%mod;
        }
    }
    cout<<f[n]<<endl;
    return 0;
}

4.2 解法2

dp思想:
1.状态划分: f [ i , j ] f[i,j] f[i,j]表示总和为 i i i,总个数为 j j j的方案数
2.状态计算:划分为方案中的最小值是否为1
为1: f [ i , j ] = f [ i − 1 , j − 1 ] f[i,j]=f[i-1,j-1] f[i,j]=f[i1,j1]
不为1: f [ i , j ] = f [ i − j , j ] f[i,j]=f[i-j,j] f[i,j]=f[ij,j]
二者相加 f [ i , j ] = f [ i − 1 , j − 1 ] + f [ i − j , j ] f[i,j]=f[i-1,j-1]+f[i-j,j] f[i,j]=f[i1,j1]+f[ij,j]

#include<bits/stdc++.h>
using namespace std;
const int N=1010,mod=1e9+7;
int f[N][N];//f[i,j]表示从前i个选,体积恰好为j的方案数
int main()
{
    int n;
    cin>>n;
    f[0][0]=1;//一个数都不选
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=i;j++)//有j-1,j最好从1开始
        {
            f[i][j]=(f[i-1][j-1]+f[i-j][j])%mod;
        }
    }
    int res=0;
    for(int i=1;i<=n;i++)
    {
        res=(res+f[n][i])%mod;
    }
    cout<<res<<endl;
    return 0;
}
解法2例题

落谷1025

5.数位统计dp

acwing 338
dp思路
求1~n中,x出现的次数:
例如:n=abcdefg , 求x在第4位出现的次数
分类讨论:
①前三位:000-abc-1,x,后三位000~999. 方案数:abc*1000
②前三位:abc,x
…2.1 d < x , 后三位:无解. 方案数:0
…2.2 d = x , 后三位:000~dfg. 方案数:dfg+1
…2.3 d > x , 后三位:000~999. 方案数:1000
注意:
1.当判断x在第1位出现的次数时,不存在情况①
2.当x=0且在分类①时,因为不能前导全0,因此得从001开始,(这一步特判即可)

#include<bits/stdc++.h>
using namespace std;
int get(vector<int>num,int l,int r)
{
    int res=0;
    for(int i=l;i>=r;i--)
    {
        res=res*10+num[i];
    }
    return res;
}
int count(int n,int x)
{
    if(!n)return 0;
    vector<int>num;
    while(n)
    {
        num.push_back(n%10);
        n/=10;
    }
    n=num.size();
    int res=0;
    for(int i=n-1-!x;i>=0;i--)//x==0时i=n-2
    {
        if(i<n-1)
        {
            res+=get(num,n-1,i+1)*pow(10,i);
            if(!x)res-=pow(10,i);
        }
        if(num[i]==x) res += get(num,i-1,0)+1;
        else if(num[i]>x) res += pow(10,i);
    }
    return res;
}
int main()
{
    int a,b;
    while(cin>>a>>b,a||b)
    {
        if(a>b)swap(a,b);
        for(int i=0;i<10;i++)
        {
            cout<<count(b,i)-count(a-1,i)<<' ';
        }
        cout<<endl;
    }

    return 0;
}

6.状态压缩dp

6.1 AcWing 291. 蒙德里安的梦想

acwing 291
核心:
摆放的小方格方案数等价于横着摆放的小方格方案数
dp思想:
1.状态表示: f [ i , j ] f[i,j] f[i,j]表示前 i − 1 i-1 i1列已经确定,且从第 i − 1 i-1 i1列伸出的小方格在第 i i i列的状态为 j j j 的方案个数( j j j表示每一行若从第 i − 1 i-1 i1列伸出记为1,反之为0的二进制数)。
用一个 n n n位的二进制数,每一位表示一个物品, 0 / 1 0/1 0/1表示不同的状态。因此可以用 0 → 2 n − 1 0→2^n−1 02n1( n n n位二进制对应的十进制数)中的所有数来枚举全部的状态
2.状态计算:
2.1 状态划分:是依据 i − 2 i-2 i2列伸到 i − 1 i-1 i1列的不同状态 k k k来划分 ( 0 ≤ k ≤ 2 n − 1 , k ∈ Z ) (0≤k≤2^n-1,k\in Z) (0k2n1kZ)
如何判断当前方案是否合法?
遍历每一列,i列的方案数只和 i − 1 i-1 i1列有关系
j & k = = 0 j\&k==0 j&k==0,即 i − 2 i-2 i2列伸到 i − 1 i-1 i1的小方格和 i − 1 i-1 i1列放置的小方格不重复。(表示两个数相与,如果有1位相同结果就不是0, j & k = = 0 j\&k==0 j&k==0表示 k k k j j j没有1位相同, 即没有1行有冲突。)
每一列,所有连续着空着的小方格必须是偶数个
2.2 状态转移:(限制条件: i − 1 i-1 i1列非空白位置可以不能放置小方格),在i列不同的放置方法就是不同的集合划分。
问题:第 i − 2 i-2 i2列伸到 i − 1 i-1 i1列的状态为 k k k,是否能成功转移到 第 i − 1 i-1 i1列伸到 i i i列的状态为 j j j?
需要满足如下条件:
j & k = = 0 j\&k==0 j&k==0 i − 2 i-2 i2列伸到 i − 1 i-1 i1的小方格和 i − 1 i-1 i1列放置的小方格 不重复。
每一列,所有连续着空着的小方格必须是偶数个( j ∣ k j|k jk就是当前第 i − 1 i-1 i1列的到底有几个1,即哪几行是横着放格子的)。
此时 f [ i , j ] = f [ i , j ] + f [ i − 1 , k ] f[i,j]=f[i,j]+f[i-1,k] f[i,j]=f[i,j]+f[i1,k]
f [ m , 0 ] f[m,0] f[m,0]:列数从 0 0 0开始计数, m m m列不放小方格,前 m − 1 m-1 m1列已经完全摆放好并且不伸出来的状态

#include<bits/stdc++.h>
using namespace std;
const int N=12,M=1<<N;
long long f[N][M];// 第一维表示列, 第二维表示所有可能的状态,摆放的小方格方案数等价于横着摆放的小方格方案数
bool st[M];//判断该状态是否合法,即不存在连续奇数个0
int n,m;
int main()
{
    while(cin>>n>>m,n||m)
    {
        memset(f,0,sizeof(f));
        for(int i=0;i<1<<n;i++)//枚举状态
        {
            st[i]=true;
            int cnt=0;// cnt 为当前已经存在多少个连续的0
            for(int j=0;j<n;j++)//通过位操作,i状态下j行是否放置方格,0就是不放,1就是放
            {
                if(i>>j&1)//i的第j位是否为1,如果放置小方块使得连续的空白格子数成为奇数,这样的状态就是不行的
                {
                    if(cnt&1)st[i]=false;//cnt有连续奇数个0
                    cnt=0;
                }
                else cnt++;//不放置小方格
            }
            if(cnt&1)st[i]=false;// 扫完后要判断一下最后一段有多少个连续的0
        }
        f[0][0]=1;
        for(int i=1;i<=m;i++)
        {
            for(int j=0;j<1<<n;j++)
            {
                for(int k=0;k<1<<n;k++)
                {
                    if((j&k)==0&&st[j|k])f[i][j]+=f[i-1][k];
                    // j & k == 0 表示 i 列和 i - 1列同一行不同时捅出来
                    // st[j | k] == 1 表示 在 i 列状态 j, i - 1 列状态 k 的情况下是合法的,j | k不存在连续奇数个0
                }
            }
        }
        cout<<f[m][0]<<endl;
    }
    return 0;
}

6.2 AcWing 91. 最短Hamilton路径

acwing 91
dp思想:
1.状态表示: f [ i , j ] f[i,j] f[i,j]表示从 0 0 0走到 j j j,走过所有点的状态为 i i i的最短哈密顿路径。可以用 0 → 2 n − 1 0→2^n−1 02n1( n n n二进制对应的十进制数)中的所有数来枚举全部的状态 i i i
状态 i i i为二进制数,每一位数表示该点是否走过, 1 1 1为走过, 0 0 0为未走过, i i i 0 0 0走到 j j j走过所有点的路径状态
2.状态计算:状态划分讨论j的前一个点k ( 0 ≤ k ≤ n − 1 , k ∈ Z ) (0≤k≤n-1,k\in Z) (0kn1kZ)
前一个点为 k k k的条件: i > > j & 1 = = 1 i>>j\&1==1 i>>j&1==1( i i i的第 j j j位为1)
f [ i , j ] = m i n ( f [ i , j ] , f [ i − ( 1 < < j ) , k ] + w [ k , j ] ) f[i,j]=min(f[i,j],f[i-(1<<j),k]+w[k,j]) f[i,j]=min(f[i,j],f[i(1<<j),k]+w[k,j])

#include<bits/stdc++.h>
using namespace std;
const int N=21,M=1<<N;
int f[M][N];//f[i,j]表示从0-j走过状态i的最短哈密顿路径
//状态压缩,用二进制数表示该点是否走过,1为走过,0为未走过
int w[N][N];
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<n;j++)
        {
            cin>>w[i][j];
        }
    }
    memset(f,0x3f,sizeof(f));
    f[1][0]=0;
    for(int i=0;i<1<<n;i++)
    {
        for(int j=0;j<n;j++)
        {
            if(i>>j&1)//i的第j位是否为1
            {
                for(int k=0;k<n;k++)//状态划分讨论j的前一个点k
                {
                    if(i>>k&1)//或者写成if((i-(1<<j))>>k&1),i-(1<<j)代表排除j点的状态
                    {
                        f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);
                    }
                }
            }
        }
    }
    cout<<f[(1<<n)-1][n-1]<<endl;
    return 0;
}

7.树形dp

acwing 285
dp思想:
状态表示: f [ u , 0 ] f[u,0] f[u,0]为从以 u u u为根节点的子树,不选 u u u的方案最大值; f [ u , 1 ] f[u,1] f[u,1]为从以 u u u为根节点的子树,选 u u u的方案最大值
状态计算:按选u和不选u划分
选u: f [ u , 1 ] = ∑ s ∈ s o n s f [ s , 0 ] f[u,1]= {\sum\limits_{s \in sons} f[s,0] } f[u,1]=ssonsf[s,0]
不选u: f [ u , 0 ] = ∑ s ∈ s o n s m a x ( f [ s , 0 ] , f [ s , 1 ] ) f[u,0]= {\sum\limits_{s \in sons} max(f[s,0],f[s,1])} f[u,0]=ssonsmax(f[s,0],f[s,1])

#include<bits/stdc++.h>
using namespace std;
const int N=6010;
int h[N],e[N],ne[N],idx;
int f[N][2];//f[u][0]为从以u为根节点的子树,不选u的方案最大值;f[u][1]为从以u为根节点的子树,选u的方案最大值
int happy[N];
bool has_father[N];
int n;
void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u)
{
    f[u][1]=happy[u];
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        dfs(j);
        //状态划分
        f[u][0]+=max(f[j][0],f[j][1]);//不选u,选或不选u的儿子
        f[u][1]+=f[j][0];//选u,不选u的儿子
    }
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)cin>>happy[i];
    memset(h,-1,sizeof(h));
    for(int i=0;i<n-1;i++)
    {
        int a,b;
        cin>>a>>b;
        add(b,a);
        has_father[a]=true;
    }
    int root=1;
    while(has_father[root])root++;//找根节点
    dfs(root);
    cout<<max(f[root][0],f[root][1])<<endl;
    return 0;
}

8.记忆化搜索

acwing 901
dp思想:
状态表示:f[i,j]表示所有从(i,j)开始滑的路径的最大值
状态划分:按往上下左右状态划分
左: f [ i , j − 1 ] + 1 f[i,j-1]+1 f[i,j1]+1
右: f [ i , j + 1 ] + 1 f[i,j+1]+1 f[i,j+1]+1
上: f [ i − 1 , j ] + 1 f[i-1,j]+1 f[i1,j]+1
下: f [ i + 1 , j ] + 1 f[i+1,j]+1 f[i+1,j]+1
f [ i , j ] = m a x ( f [ i , j − 1 ] + 1 , f [ i , j + 1 ] + 1 , f [ i − 1 , j ] + 1 , f [ i + 1 , j ] + 1 ) f[i,j]=max(f[i,j-1]+1,f[i,j+1]+1,f[i-1,j]+1,f[i+1,j]+1) f[i,j]=max(f[i,j1]+1,f[i,j+1]+1,f[i1,j]+1,f[i+1,j]+1)

#include<bits/stdc++.h>
using namespace std;
const int N=610;
int h[N][N];
int f[N][N];//f[i,j]表示起点为(i,j)的最大滑雪长度
int n,m;
int dp(int x,int y)
{
    int &v=f[x][y];//等于把f[x][y]简化成了v,如果v发生变化,f[x][y]也会随之变化
    if(v!=-1)return v;//该状态被算过
    v=1;
    int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
    for(int i=0;i<4;i++)//按上下左右状态划分
    {
        int a=x+dx[i],b=y+dy[i];
        if(a>=1&&a<=n&&b>=1&&b<=m&&h[a][b]<h[x][y])
        {
            v=max(v,dp(a,b)+1);
        }
    }
    return v;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            cin>>h[i][j];
        }
    }
    memset(f,-1,sizeof(f));
    int res=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            res=max(res,dp(i,j));//找最大
        }
    }
    cout<<res<<endl;
    return 0;
}

dp练习(个人题解)

1.【动态规划1】动态规划的引入

1.1 P1002 [NOIP2002 普及组] 过河卒(线性dp)

落谷P1002
dp思路: f [ i , j ] f[i,j] f[i,j]表示到坐标为 ( i , j ) (i,j) (i,j)的方案数
f [ i , j ] = f [ i − 1 , j ] + f [ i , j − 1 ] f[i,j]=f[i-1,j]+f[i,j-1] f[i,j]=f[i1,j]+f[i,j1]
落谷提交好像下标要严格大于0,调了好久才发现

#include<bits/stdc++.h>
using namespace std;
const int N=100;
bool check(int x,int y,int hx,int hy)
{
    if(((x-hx)*(x-hx)+(y-hy)*(y-hy))==5||(x==hx&&y==hy))//马的控制点和马本身
        return false;
    return true;
}
int f[N][N];
int main()
{
    int dx,dy,hx,hy;
    cin>>dx>>dy>>hx>>hy;
    dx++,dy++,hx++,hy++;
    f[1][1]=1;
    for(int i=1;i<=dx;i++)
    {
        for(int j=1;j<=dy;j++)
        {
            if(i==1&&j==1)continue;
            if(check(i,j,hx,hy))f[i][j]=f[i-1][j]+f[i][j-1];
            
        }
    }
    cout<<f[dx][dy]<<endl;
    return 0;
}
1.2 P1802 5 倍经验日(01背包变形)

落谷P1802
dp思想: f [ i , j ] f[i,j] f[i,j]表示从前i个人中选且总使用的药数量小于j的最大值
划分为:
1.必赢 ( j ≥ v [ i ] ) (j≥v[i]) (jv[i])
 1.1 主动输(不浪费药)
 1.2 主动赢
f [ i , j ] = m a x ( f [ i − 1 , j ] + l o s e [ i ] , f [ i − 1 , j − v [ i ] ] + w i n [ i ] ) f[i,j]=max(f[i-1,j]+lose[i],f[i-1,j-v[i]]+win[i]) f[i,j]=max(f[i1,j]+lose[i],f[i1,jv[i]]+win[i])
2.必输( j < v [ i ] j<v[i] j<v[i],不浪费药): f [ i , j ] = f [ i − 1 , j ] + l o s e [ i ] f[i,j]=f[i-1,j]+lose[i] f[i,j]=f[i1,j]+lose[i]

//二维
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int f[N][N];
int lose[N],w[N],v[N];
int n,m;
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>lose[i]>>w[i]>>v[i];
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=m;j++)
        {
            if(j>=v[i])
                f[i][j]=max(f[i-1][j]+lose[i],f[i-1][j-v[i]]+w[i]);
            else 
                f[i][j]=f[i-1][j]+lose[i];
        }
    }
    cout<<5ll*f[n][m]<<endl;
    return 0;
}
//一维
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int f[N];
int lose[N],w[N],v[N];
int n,m;
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>lose[i]>>w[i]>>v[i];
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=m;j>=v[i];j--)
        {
            f[j]=max(f[j]+lose[i],f[j-v[i]]+w[i]);
            
        }
        for(int j=v[i]-1;j>=0;j--)
            f[j]+=lose[i];
    }
    cout<<5ll*f[m]<<endl;
    return 0;
}
1.3 P1616 疯狂的采药(完全背包问题)

落谷P1616
模板题

#include<bits/stdc++.h>
using namespace std;
int n,m;
const int M=1e7+10;
long long f[M];
int v[M],w[M];
int main()
{
    cin>>m>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>v[i]>>w[i];
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=v[i];j<=m;j++)
        {
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}
1.4 P1048 [NOIP2005 普及组] 采药(01背包问题)

落谷P1048
模板题

#include<bits/stdc++.h>
using namespace std;
int n,m;
const int M=1e7+10;
long long f[M];
int v[M],w[M];
int main()
{
    cin>>m>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>v[i]>>w[i];
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=m;j>=v[i];j--)
        {
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}
1.5 P4017 最大食物链计数(记忆化搜索)

落谷P4017
邻接表存图,最大食物链为起点入度为0,终点出度为0的路线
我的dp思想: f [ i ] f[i] f[i]表示起点为i,终点出度为0的路线的数量,深搜计算至出度为0的点
f [ u ] = ∑ j ∈ u 的 后 继 f ( j ) f[u]={\sum\limits_{j \in u的后继} f(j)} f[u]=juf(j)

#include<bits/stdc++.h>
using namespace std;
const int N=5e3+10,M=5e5+10,mod=80112002;
int h[N],e[M],ne[M],idx;
int in[M];//入度
int out[M];//出度
int n,m,ans;
int f[N];//f[i]表示起点为i,终点出度为0的路线的数量
void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

int dfs(int u)
{
    if(!out[u])return 1;
    if(f[u])return f[u];//算过就不算了
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        f[u]=(f[u]+dfs(j))%mod;
    }
    return f[u];
}
int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof(h));
    for(int i=0;i<m;i++)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);
        in[b]++,out[a]++;
    }
    for(int i=1;i<=n;i++)
        if(!in[i])ans=(ans+dfs(i))%mod;
    cout<<ans<<endl;
    return 0;
}
1.6 P2196 [NOIP1996 提高组] 挖地雷(记忆化搜索)

落谷P2196
我的dp思路:f[i]表示以i为起点的所有路线挖到的最多地雷数
f [ u ] = m a x ( f [ u ] , f [ j ] + a [ u ] ) f[u]=max(f[u],f[j]+a[u]) f[u]=max(f[u],f[j]+a[u]),j为u的后继

#include<bits/stdc++.h>
using namespace std;
const int N=100;
int h[N],e[N],ne[N],idx;
int a[N];
int f[N];
int n;
void add(int a ,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int nx[N];//nx[u]=j表示u->j,u的后继为j
int dfs(int u)
{
    f[u]=a[u];
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        int k=dfs(j);
        if(f[u]<k+a[u])
        {
            f[u]=k+a[u];
            nx[u]=j;
        }        
    }

    return f[u];
}
void path(int u)
{
    if(u==0)return;
    cout<<u<<" ";
    path(nx[u]);
}
int main()
{
    cin>>n;
    memset(h,-1,sizeof h);
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n-1;i++)
    {
        for(int j=i;j<=n-1;j++)
        {
            int num;
            cin>>num;
            if(num)
            {
                add(i,j+1);
            }
        }
    }
    int ans=-1,id;
    for(int i=1;i<=n;i++)dfs(i);      
    for(int i=1;i<=n;i++)
    {
        if(ans<f[i])
        {
            ans=f[i];
            id=i;
        }    
    }
    path(id);
    cout<<endl;
    cout<<ans<<endl;
    return 0;
}

2.【动态规划2】线性状态动态规划

2.1 P1020 [NOIP1999 普及组] 导弹拦截(线性dp)

落谷P1020
题意为求最长不上升子序列和最长上升子序列

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n;
int a[N];
int q[N];//存的是以长度为i的上升子序列中末尾元素最小的数
int d[N];
int main()
{
    while(~scanf("%d",&a[++n]));//读入数据方法
    n--;
    for(int i=1;i<=n;i++)cin>>a[i];
    int len=0;
    //最长上升子序列
    for(int i=1;i<=n;i++)//二分找到q数组中最大的小于当前数a[i]的数的下标r
    {
        int l=0,r=len;
        while(l<r)
        {
            int mid=l+r+1>>1;
            if(q[mid]<a[i])l=mid;
            else r=mid-1;
        }
        len=max(len,r+1);
        q[r+1]=a[i];//将a[i]赋值给q[r+1]
    }
    int len1=0;
    //最长不上升子序列
    for(int i=1;i<=n;i++)//二分找到d数组中最大的大于等于当前数a[i]的数的下标r
    {
        int l=0,r=len1;
        while(l<r)
        {
            int mid=l+r+1>>1;
            if(d[mid]>=a[i])l=mid;
            else r=mid-1;
        }
        len1=max(len1,r+1);
        d[r+1]=a[i];//将a[i]赋值给d[r+1]
    }
    cout<<len1<<endl;
    cout<<len<<endl;

    return 0;
}

总结

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值