AcWing算法基础课

第一讲。语法基础

qiuk_sort(q,l.r)快排思想基于分治):O(nlog2n)

①以left、right、中间点或随机点为分界线

排序(调整)区间:小于等于x的在左边区间,大于等于x在右边区间。注:两区间可能长度不同(*最难部分)O(log2n)

③递归处理左右两端O(n)

785快速排列

//785

qiuk_sort()

{

    if(l>=r) return q[l];

    int i=l-1,j=r+1;int x=q[l];

    while(j>i)

    do i++ while(q[i]<x);

    do j-- while(q[j]>x);

    if(i<j)

    swap();

}

qiuk_sort(q[],l,j);

qiuk_sort(q[],j+1,r);

786.第k个数

//786
qiuk_sort()
{
    if(l>=r) return q[l];
    int i=l-1,j=r+1;int x=q[l];//x=(l+r)/2;
    while(j>i)
    while(q[++i]<=x);
    while(q[--j]>=x);
    if(i<j)//
    swap();
}
int sl=j-i+1;//
if(sl>k) return qiuk_sort(l,j,k);//
else return qiuk_sort(j+1,r,k-sl);//k-sl

2.merge_sort(q,l,r)归并排序思想(基于分治):O(nlogn)

①以中间点为分界线分为left、right

递归排序left、right  O(logn)

归并两个有序数组合并为一个(*最难部分)O(n)

求逆序对数量(2种情况);都在mid左/右merge_sort(L,mid)/merge_sort(mid+1,R),mid左右各一个(即归并的过程)

(逆序对:从数组中任取两个数,前面一个比后一个大)

787.归并排序

//787

void merge_sort(int q[],int l,int r)

{

    if(l>=r) return;

int mid=l+r>>1;

merge_sort(q[],l,mid); merge_sort(q[],mid+1,r);

    int temp[],k=0;

    int i=l,j=mid+1;

    while(i<=mid&&j<=r)

            if(q[i]<=q[j])  temp[k++]=q[i++];

            else temp[k++]=q[j++];

    while(i<=mid) temp[k++]=q[i];//左边没有循环完情况

    while(j<=r) temp[k++]=q[j];// //右边没有循环完情况

    for(i=1,j=0,i<n;i++,j++) q[i]=tem[j];
}

788逆序对的数量

LL merge_sort(int q[],int l,int r)
{
	if(l>=r) return 0;// q[l]
    int mid=l+r>>1;//
    LL res=merge_sort(q[],l,mid)+merge_sort(q[],mid=+1,r);
int temp[],k=0;int i=l,j=mid+1;//i=l-1;j=r+1
while(i<=mid&&j<=r)//j>i***
            if(q[i]<=q[j])  temp[k++]=q[i++];//不是逆序res不累加
            else temp[k++]=q[j++];res+=mid-l+1;
    while(i<=mid) temp[k++]=q[i];//=
    while(j<=r) temp[k++]=q[j];
    for(i=1,j=0,i<n;i++,j++) q[i]=tem[j];//i=0
return res;//***

3.二分查找(整数划分)本质:给定区间上定义某种性质,是的分界点一边满足另一边不满足该性质(注:有单调性一定可以二分,能二分不一定有单调性)

注if结果中l=mid,则条件mid=(l+r+1)/2;若结果r=mid,则mid=(l+r)/2

二分查找思想:

确定边界,找中间值mid=(l+r)/2,

②用check()判断其是否满足某种性质,

true/false对应情况如何更新区间

789整数二分

//789整数二分
int main()
{
    int m,n,q[];
    scantf("%d%d",n,m);
    for(int i=0;i<n,,i++) scantf("%d",q[i]);
    While(m--)
    {
		int x;
        scantf(“%d”,&x);
        int l=0,r=n-1;
        while(l<r)
        {
            int mid=l+r>>1;
            if(q[mid]>=x) r=mid;
            else l=mid+1;
        }
        if(q[l]!=x) printf("%d%d",-1,-1);
		else
        {
      		while(l<r)
            mid=l+r+1>>1;
            if(q[mid]>=x)  r=mid-1;
            else l=mid;
        }
    }
return 0;
}

790数的三次方根(浮点数的二分)

int main()
{
    double x;//int
    scantf("%d",x);
    double l=-10000,r=10000;
    double mid=(l+r)/2;
    while(r-l>1e-8)//l<r,1*10的负8次方
    {
        if(mid*mid*mid>=x) r=mid;
        else l=mid;
       
}
printf("%lf\n",mid);
}
return 0;

795前缀和

求前缀和Si:循环数组1~i个数 s[i]=s[i-1]+a[i],

将s[0]定义为0à方便处理边界

前缀和作用:快速求出原数组中一段数[l,r]的和,一次运算计算出区间和S[r]-S[l-1]

#include <stdio.h>

const int N=100001;

int m,n;

int q[N],s[N];

int main()

{

    scantf("%d%d",&n,&m);

    for(int i=1;i<=n;i++)

        scantf("%d",q[i]);

       while(m--)

{

int l,int r;

              scantf(“%d%d”,&l,&r);

           for(int j=1;j<=l;j+)

            s[j]=s[j-1]+q[j];

s[l,r]=s[r]-s[l-1];

printf("%d",s[l,r]);

}

    return 0;

}

第二讲。数据结构

链表与邻接表、栈与队列都有多种实现方式:

①结构体+指针实现链表:

struct Node

{

        int val;       

        Node *next;

}//这种实现方式每次创建一个新链表是就调用new Node(),但是此函数操作很慢-->一般在笔试题中链表大小限制,容易超时,但在面试题中常用

②数组模拟链表(单链表/双链表);

826. 单链表(数组模拟单链表,用的最多的是写邻接表——应用:存储树和图

单链表—(除首尾)每个结点存一个val和一个next指针--->定义一个val数组(e[N]),next数组ne[N]

#include <iostream>
//#include <algorithm>
unsing namespace std;
const int N=100010;
int head,e[N],ne[N],idx;//idx存储当前用到点的地址,相当于指针
//初始化
void init()
{
    head=-1;
    idx=0;
}
//新节点插入到头节点
void add_to_heaad(int x)
{
    e[idx]=x;//把x存储
    ne[idx]=head;//新加入点指向head指针节点
    head=idx;//将head指向新加入节点
    idx++;//idx移动到下一个位置
}**算法中大概率都是把新结点插入到头节点位置
//新节点插入到第k个位置(下标k-1)后/即下标为k的节点
void add(int k,int x)//x插入到第k位置后
{
    e[idx]=x;//先存储x
    ne[idx]=ne[k];//新节点指针指向第k个节点的下一个位置
    ne[k]=idx;//k点指向新节点
    idx++;
}
//删除第k个数(下标k-1)后、即下标为k的节点
void remove(int k)
{
    ne[k]=ne[ne[k]];//将k的指针指向k的指针的指针   
}
int main()
{
    int m;
    cin>>m;
    init();
    while(m--)
    {
        int k,x;
        char op;
        cin>>op;
        if(op=='H')
        {
            cin>>x;
            add_to_heaad(x);
        }
        else if(op=='D')
        {
            if(!k) head=ne[head];
            cin>>k;
            remove(k-1);
        }
        else
        {
            cin>>k>>x;
            add(k-1,x);
        }
    }
    for(int i=head;i!=-1;i=ne[i])//最后把整个链表遍历一遍
    cout<<e[i]<<' ';//每次先输出当前i的值
    cout<<endl;
    
    return 0;
}

827. 双链表模拟双链表——一般用于优化某些问题



828栈的定义stk[N],tt表栈顶下标

int stk[N], tt;

stk[ ++ tt]=x;// 插入

tt --;// 弹出

if(tt > 0) not empty else empty// 判空

stk[tt];//栈顶

3302. 表达式求值
 

829队列定义q[N],hh表对头,tt表对尾

int q[N], hh, tt =-1;

q[++tt]=x;//插入

hh ++;// 弹出

if (hh <= tt) not empty else empty//判空

q[hh] //取出队头元素

830. 单调栈


154. 滑动窗口


831. KMP字符串


835. Trie字符串统计

143. 最大异或对

并查集(836\837\240)代码短,思路精巧,在笔试、面试、竞赛中常用算法

并查集支持的操作:①将两个集合合并②快速两个元素是否在一个集合中③在接近O(1)的时间内完成合并、查找操作

基本思想:每一个集合用一个树来表示,每个节点的编号是它根节点的编号;树中每个节点都用p[x]存储其父节点是谁;当想求某个点属于哪个集合时,直接找这个点的父节点,判断其是否是树根,如果不是递归再往上找,知道找到根为止;

问题1如何判断一个点是树根:if(p[x]==x)

问题2如何求x的集合编号:while(p[x]!=x)  x=p[x];//只要x不是树根就往上走,走到树根为止

问题3如何合并两个集合:加一条边,将其中一棵树的根节点直接插入到另一颗树的某个位置即可

假设px 是x的集合编号,py是y的集合编号。p[x]=y即可

并查集的一个优化(路径压缩):子节点往上走找到根节点时,直接将路径上所有节点的父节点更改为根节点,后面查的时候就只需要往上走一步(并查集加完此操作后,基本可以再O(1)时间复杂度内完成查找)


836. 合并集

#include<iostream>
using namespace std;
const int N=1e5+10;//10000+10
int p[N];//定义存父节点的数组
int n,m;

int find(int u)//并查集中核心操作find(),返回u所在集合的编号(u的祖宗节点)+路径压缩
{
    if(p[u]!=u) p[u]=find(p[u]);//如果p[u]!=u,说明u不是根节点;将其父节点等于它的祖宗节点p[u]=find(p[u])
    return p[u];//最后返回父节点p[u]
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) p[i]=i;//本题每个数各自在一个集合中,把所有父节点p[i]赋值成自己i
    
    while(m--)//m个操作,每个操作有两种
    {
        char op;
        int a,b;
        cin>>op;
        cin>>a>>b;
        if(op=='M')//合并操作
        {
            p[find(a)]=find(b);//让a的祖宗节点的父亲等于b的祖宗节点,即将a插到b树
        /*  int x=find(a),y=find(b);
            if(x!=y)
            {
                p[x]=b;
            } 
        */
        }
        else //查找操作
        {
            if(find(a)==find(b))//说明两个节点再一个集合中
                cout<<"Yes"<<endl;
            else cout<<"No"<<endl;
        }
    }
    return 0;
}

 837. 连通块中点的数量

​​​​​​​

 240. 食物链


 838. 堆排序

 839. 模拟堆


 840. 模拟散列表


 841. 字符串哈希

第三讲搜索与图论

DFS没有固定常用框架(暴力搜索)-->考虑遍历所有结点的顺序(搜索顺序时一棵树)

842数字排列问题

#include <stdio.h>
const int N=10;
int n;
int path[N];//存储路径状态
bool st[N];//判断遍历点是否已经被用过
int dfs(int u)
{
    if(u==n)//当走到第n个位置时,说明已经遍历完,直接输出
    {
        for(int i=0;i<=n;i++) printf("%d",path[i]);
        puts(" ");
        return;//作用相当于break,中断循环
    }
for(int i=1;i<=n,i++)//枚举当前可以遍历的数
if(!st(u)//找到一个没有被用过的数
    {
        path[u]=i;
        st[i]=true;
        dfs(u+1);
        st[i]=false;
    }
}
int main()
{
    scantf("%d",&n);
    dfs(0);
    return 0;//返回非void函数
}

843,n皇后问题

解一:全排列搜索(逐行枚举)解法

#include <stdio.h>
const int N=10;
int p[N][N];
boolean col[N],dg[N],udg[N];
int n,u;
void dfs(u)
{
    if(u==n)
    {
        for(j=0;j<=n,j++)
        printf("%d",p[i][j]);
        puts(" ");
        return;
    }
    for(i=0;i<=n,i++)
    if(!col[i]=false&&!dg[i+u]&&!udg[i-u+n])
    {
        p[u][i]='Q';
        col[i]=false&&dg[i+u]&&udg[i-u+n=true;
        dfs(u+1);
        col[i]=false&&dg[i+u]&&udg[i-u+n=false;
        p[u][i]='.';
    }
}
int main()
{
    scantf("%d",&n);
    //dfs(0);
    for(int i=0;i<=n,i++)
    {
        for(int j=0;j<=n;j++)
        p[j][j];
    }
    dfs(0);
    return 0;
}

BFS有常用的固定框架,(只有所有边权值都为1时,才能用BFS求最短路,)

BFS需要队列Q[N][N]

*BFS时间复杂度高,最短路径时间复杂度低

844.走迷宫

g[N][N]存迷宫图

d[N][N]存迷宫中每个点到起点的距离

注: memset()多用于清空数组.原型是memset(buffer, 0, sizeof(buffer))

memset(buffer, c, count) :buffer:指针或数组, c:赋给buffer值,count:buffer长度.

往上下左右四个方向扩展à用向量表示

#include <stdio.h>
#include <string>
#include <queue>//bfs中一般需要队列
/*手写队列代码实现:
PII q[N*N]
*/
const N=110;
int m,n;
bool st[N];
int g[N][N];//g数组存图
int d[N][N];//d数组存每个点到起点的距离
void bfs(int u)
{
        int hh=0,tt=0;//对头hh,队尾tt
        q[0]={0,0};
        memset(d,-1.sizeof d);//所有距离初始化为-1.表没走过
        d[0][0]=0;//表已经走过
        int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
        while(hh<=tt)//队列不空
        {
            auto t=q[hh++];//每次取出对头
            for(int i=0;i<4;i++)//枚举当前结点上下左右四个方向
            {
                int x=t.first+dx[i],y=t.second+dy[i];//沿着某个方向走到达的点
                if(x>=0&&x<n&&y>=0&&y<m&&g[x][y]==0&&d[x][y]==-1)//如果该点在边界内且可以走
                {
                    d[x][y]=d[t.first][t.second]+1;//将该点更新
                    q[++tt]={x,y};//扩展出来
                }
            }
        }
        return d[][];//输出右下角点的距离
}
int main()
{
    scantf("%d%d",&n,&m);//n行m列
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        scantf("%d",g[i][j]);//读入数组
    bfs(u);
    return 0;
}

*845.八数码

难点1状态表示复杂 : 直接用一个字符串表示一个状态

难点2如何记录每个状态的距离dist数组:定义一个队列queue<string>,把状态存到队列中 ,dist[]用哈希表存储

难点3实现状态转移

代码实现:

#include <iostream>
#include<algorithm>//算法库
#include<unordered_map>//无序容器
#include<queue>//广度优先遍历,需定义队列
using namespace std;//命名空间
int bfs(string start)
{
    string end="12345678X";//定义终点
    queue string q;//广度优先定义一个队列
    unordered_map<string,int>d//定义一个距离数组
    q.push(start);//start放入队列中
    d[start]=0;//表最开始起点到起点距离
    int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
    while(q.sizeof())//队列不空
    {
        auto t=q.front();//对头元素
        q.pop();//去除对头
        if(t==end) return d[t];//如果t等于终点提前结束
        
        //状态转移**
        int k=t.find('x');//用k存储当前遍历点x的位置,find('x')会返回x的下标
        int x=k/3,y=k%3;//x转换为某状态矩阵的横纵坐标,即将一维数组下标转换为2为数组下标的公式**
        for(int i=1;i<=4,i++)
        {
            int a=x+dx[i],b=y+dy[i];//x上下左右偏移量表示
            if(a>=0&&a<3&&b>=0&&b<3)//如果没有出界
            /*转移目标:将当前x,y转移到上下左右a,b-->交换x,y与a,b */
            {
                 swap(t[k],t[a*3+b]);//交换大当前点与上/下/左/右的状态跟新
                 if(!d.count(t))//如果当前t之前没有搜过
                 {
                     d[t]=distance+1;//更新新状态的距离
                     q.push(t);//加入到队列中
                 }
                 swap(t[k],t[a*3+b]);//恢复状态
            }
        }//枚举x上下左右四个数
    }广度优先遍历过程**
    return -1;//while/广度优先搜索中找不到终点,说明到不了终点
}
int main()
{
    string start;//初始状态
    for(int i=1;i<=9;i++)
    {
        char c;
        cin>>c;
        start+=c;
    }//读入九个数据
    cout<<bfs(start)<<endl;
    return 0;
}

846. 树的重心


847. 图中点的层次


848. 有向无环图的拓扑序列(图的广度优先的应用)---答案不唯一

#include <iostream>
#include <algorithm>
//#include <unordered_map>
//#include <queue>
#include <cstring>//
using namespace std;
const int N=100010;
int h[N],e[N],ne[N],idx;//链接表的存储方式
int q[N],d[N];//q是队列,d是点的入度
int m,n;
void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}//将每个边加入
bool topsort()
{
    int hh=0,tt=-1;//定义队列头、尾
    for(int i=1;i<=n;i++)//从前往后遍历所有入度为0的点
    if(!count())//入度为零
        q[++tt]=i;//插入到队列中,**经典写法:数组模拟队列(前面有)
    while(hh<=tt)//队列不空
    {
        int t=q[hh++];//取出对头元素,**出队顺序即拓扑序列
        for(int i=h[t];i=-1;i=ne[i])//拓展对头元素
        {
            int j=e[i];//找到出边
            d[j]--;
            if(d[j]==0) q[++tt]=j;
        }
    }
    return tt=n-1;//判断是否所有点都已入队,tt=n-1说明队列一共进了n个点
}//拓扑排序
int main()
{
    cin>>n>>m;//cin>>n,m;
    memset(h,-1,sizeof h);//
    for(int i=0;i<=m;i++)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);
        d[b]=++;//更新入读
    }//输入
    if(topsort()//判断是否存在拓扑排序
    {
        for(int i=1;i<=n;i++)
        put("");
    }//如果存在输出
    else puts("-1");//如果不存在输出-1
    return 0;
}

849-854最短路:


849. Dijkstra求最短路Ⅰ

朴素版迪杰斯特拉算法(当前已经确定最短路的点存入s)——用连接矩阵存储时间复杂度O(n*n):

①先初始化距离dist[1]=0,dist[i=+∞ ②循环n次,找到不在s中的距离最近的点t③把t加入s中去,用t来更新其他距离

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=510;
int m,n;
int dist[N];
int g[N][N];//用连接矩阵存
bool st[N];//表每个点的最短路是否确定
int dijkstra()
{
    memset(dist,0x3f,sizeof dist);//初始化为负无穷
    dist[1]=0;
    for(int i=1;i<=n;i++)//迭代n次
    {
        int t=-1;
        for(int j=1;j<=n;j++)
            if(!st[j]&&(t==-1||dist[t]>dist[j]))//每次先找当前没有确定最短路长度的点中距离最小的||当前t不是最短的
                t=j;//t更新成j
        st[t]=true;//将t加入集合
        for(int j=1;j<=n;j++)//拿t更新其他点的距离
            dist[j]=min(dist[j],dist[t]+g[t][j]);
    }
    //直接返回
    if(dist[n]==0x3f3f3f) return-1;
    else return dist[n];
}
int main()
{
    cin>>n>>m;
    /*for(int i=1;i<=n;i++)//循环
          for(int j=1;j<=n;j++)
          //初始化
          if(i==j) g[i][j]==0;
          else g[i][j]=INF;//g[i][j]等于负无穷*/
    //等价于memset()
    memset(g,0x3f,sizeof g);//0x3f表无穷大常量
    while(m--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        g[a][b]=min(g[a][b],c);
    }
    int t=dijkstra();
    cout<<t<<endl;
    return 0;
}


850. Dijkstra求最短路 Ⅱ

队优化版dijkstra——用邻接表存储,时间复杂度O(n)

优化:在一堆数中找最小——用堆找 时间复杂度O(nm)-->O(mlogn)

           找不在S中的距离最小的点,时间复杂度O(n)-->O(1)

          用t更新其他点距离,时间复杂度O(m)-->O(mlogn) 堆中修改数据logn,修改m次

堆的实现方式:①手写堆(可保证堆中始终只有n个数,支持修改堆中任意元素)

②优先队列(不支持修改任意元素的操作,实现方式:每次修改都往堆中插入一个新元素;缺点:堆中中元素个数可能有m个,时间复杂度变为mlogn。

#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>//需要用到优先队列
using namespace std;
typedef pair<int,int> PII;//用堆存维护距离时对应节点编号
const int N=100010;
int m,n,idx;
int dist[N];
int h[N],e[N],ne[N],w[N];//存储方式是稀疏图,改成邻接表的形式,w[n]存权重
bool st[N];//表每个点的最短路是否确定
void add(int a,int b,int c)//邻接表中重边不影响,不需对重边做处理
{
    ne[idx]=h[a],e[idx]=b,w[idx]=c,h[a]=idx++;
}
int dijkstra()
{
    memset(dist,0x3f,sizeof dist);//初始化为负无穷
    dist[1]=0;
    priority_queue<PII,vector<PII>,greater<PII>> heap;//小根堆
    heap.push({0,1});//1号点已知最短距离,先放入1号点,更新其他点
    while(heap.sizeof())//堆中不空
    {
        auto t=heap.top();//每次找到当前距离最小的点作为堆的起点
        heap.pop();
        int ver=t.second,distance=t.first;
        if(st[ver])  continue;//如果当前点已经出来过,说明当前点时冗余,不必要再处理,直接继续
        for(int i=h[ver];i!=-1;i=ne[i])//否则用这个点更新其他点,遍历所有连边
        {
            int j=e[i];//用j来存储点编号
            if(dist[j]>distance+w[i])//如果更新成功,把j点放到优先队列中去
            {
                dist[j]=distance+w[i];
                heap.push{dist[j],j});
            }
        }
    }
    if(dist[n]==0x3f3f3f) return-1;
    return dist[n];
}
int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);//邻接表初始化为空
    while(m--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    int t=dijkstra();
    cout<<t<<endl;
    return 0;
}


853. 有边数限制的最短路


851. spfa求最短路


852. spfa判断负环


854. Floyd求最短路


858. Prim算法求最小生成树


859. Kruskal算法求最小生成树


860. 染色法判定二分图


861. 二分图的最大匹配

第五讲 动态规划DP(类似递归)

1.01背包问题

暴力法基本思想:

f[i][j]:表示只看前i个物品,总体积是i的情况下,总价值最大是多少。

result = max f[n][0~v]

f[i][j]:1.不选第i个物品f[i][j]=f[i- 1][j];

2. 第i 个物品,f[i][j]=f[i-1][j -v[i]]+w[i];

f[i][j] = max{1,2}

f[0][0]=0;//初始化

复杂度:O(n2)

暴力法优化:

第二维j,求f(i,j)时要么是j,要么是j- vi,只会用到比j自身更小的数à所有j可以拿优化成一个数组,从大到小循环

注:DP问题的所有优化都是对代码做等价变形

解法一:暴力解代码实现

#include <stdio.h>

int n,m;

int v[N],w[N];

int f[N][N];

int main()

{

    scantf("%d%d",&n,& m);

    for(int i=1;i<=n;i++)

        scantf("%d%d",v[i],w[i]);

    for(int i=1;i<=n;i++)

        for(j=0;j<=m;j++)

        {

            f[i][j]=f[i-1][j];//左半边子集第i个物品不选,优化为一维f[j]=f[j]

            if(j>=v[i]) //如果选
                f(i,j)=max(f(i-1,j),f(i-1,j-v[i])+w[i]);
        }

        int res=0;

        for(int i=0;i<=m;i++)

        res=max(res,f[n][i]);//i=0,f[n][i]

        printf("%d",res);
        return 0;
}

暴力求解优化:用一维数组实现,代码实现:

#include <stdio.h>

int n,m;

int v[N],w[N];

int f[N];

int main()

{

    scantf("%d%d",&n,& m);

for(int i=1;i<=n;i++)

   scantf("%d%d",v[i],w[i]);

     for(int i=1;i<=n;i++)

          for(j=m;j>=v[i];j--)

f[j]=max(f[j],f[j-v[i]]+w[i]);//此语句两个注释

/*1^求f(i,j)时要么是j,要么是j- vi,只会用到比j自身更小的数,因此f优化成一个数组,j从大到小循环à确保求f[j]时j-v[i]已经计算过*/

/*2^不需for()枚举0-m,f[j]就是答案 与初始化相关:

如果初始化时f[i]=0即所有f[i]初始化为0àf[m]表所有体积小于等于m的情况下最大价值而非恰好等于m时最大价值量

如果初始化时只把f[0]=0即只把f[0]初始化为0,其他初始化为负无穷,则需要枚举,确保f[m]从f[0]转移过来,从其他转移都是负无穷*/

     printf("%d",f[m]);

     return 0;

}

解法二:有限集合求最值

01背包问题(选择问题),选择问题状态表示:一般低维考虑前i个物品,后面几维限制

所有第i层只用到第i-1层—>求f(I,j)时用一个滚动数组,只需保留上一层结果,之前结果不会再用到,可删掉

状态表示:将有限集化零为整的过程,有三种(最大、最小、数量)

状态计算:对集合递归划分(注集合划分不重不漏à推状态转移方程:

f(i,j)=max{f(i-1,j),f(i-1,j-vi)+wi}

#include <stdio.h>

int n,m;

int v[N],w[N];

int f[N][N];

int main()

{

  scantf("%d%d",&n,& m);

  for(int i=1;i<=n;i++)

     scantf("%d%d",v[i],w[i]);

  for(int i=1;i<=n;i++)//i=1,

     for(j=0;j<=m;j++)//j=0

     {

        f[i][j]=f[i-1][j];//左半边子集第i个物品不选,优化为一维f[j]=f[j]

        if(j>=v[i]) //如果选

        {

            f(i,j)=max(f(i-1,j),f(i-1,j-v[i])+w[i])  

        }

printf(“%d”,f[n][m]);//f[n][m]即最大价值

return 0;

}

完全背包问题dp分析

01背包转变为完全背包问题结论:将j的循环顺序for(j=0;j<=m;j++)à变为从大到小for(j=v[i];j>=m;j--)

两者区别

01背包f[i[[j]=max(f[i-1][j],f[i-1][j-v]=w)

完全背包f[i[[j]=max(f[i-1][j],f[i][j-v]=w)

//完全背包代码实现:

#include <stdio.h>

const int N=1010;

int v[N],w[N];

int m,n;

int f[N][N];

int main()

{

    scantf("%d%d",n,m);

    for(int i=1;i<=n;i++)

        scantf("%d%d",v[i],w[i]);

    for(int i=1;i<=n;i++)

        for(int j=v[i];j<=m;j--)

        {

            f[i][j]=f[i-1][j];

            if(j>=v[i])

            f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]);

        }

        printf("%d",f[n][m]);

        return 0;

}

//完全背包代码优化:

#include <stdio.h>

const int N=1010;

int v[N],w[N];

int m,n;

int f[N];

int main()

{

    scantf("%d%d",n,m);

    for(int i=1;i<=n;i++)

        scantf("%d%d",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]);

        }

        printf("%d",f[m]);

        return 0;

}

线性DP, (DP从集合角度看)

//898数字三角形(路线问题)

#include <stdio.h>

#include <string>

int n;

const int N=500;

int w[N][N],f[N][N];//两数组可优化为一个

int main()

{

    scantf("%d",&n);

    for(int i=0;i<n;i++)

        for(int j=1;j<=i;j++)

        scantf("%d",w[i][j]);

       for(int i=0;i<n;i++)

      for(int j=1;j<=i;j++)

              f[i][j]=-INF;//数组初始化(可无),INF无穷大

   for(int j=0;j<n;j++) f[n][j]=w[n][j];

   for(int i=n-1;i<n;i++)

      for(int j=1;j<=i;j++)

      f[i][j]=max(f[i+1][j],f[i+1][j+1])+w[i][j];

    printf("%d",f[1][1]);

    return 0;

}

//897最长公共子序列

代码实现:f[i[[j]下标用到了i-1,j-1,所以下标从1

#include <stdio,h>

const int N=1000;

int n,m;

int a[N],b[N];

int f[N][N];

int main()

{

    scantf("%d%d",&n,&m);

    for(int i=1;i<=n;i++)

        scantf("%d",a[i]);

    for(int j=1;j<=m;j++)

        scantf("%d",b[j]);

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]);//先求a[i]!=b[j]的情况

//注f[i-1][j],f[i][j-1]包含f[i-1][j-1]的情况

  if(a[i]=b[j])

            f[i][j]= max(f[i][j],f[i-1][j-1]+1)//再加入相等情况

}

    printf("%d",f[n][m]);

return 0;

}

282.石子问题(区间合并)

问题:

一般大部分的区间合并问题:都是先枚举区间长度,再枚举区间左端点

3重for循环:枚举区间长度-->枚举左端点-->枚举合并位置

//在队尾插入元素,在队头弹出元素

#include <stdio.h>

const int N=1010;

int n;

int s[N];//前缀和

int f[N][N];//状态数组,f[i][j]:所有从第i堆石子到第j堆石子合并成一堆石子的合并方式

int main()

{

      scantf("%d",&n);

    for(int i=1;i<=n;i--)

        scantf("%d",s[i]);//读入石子数

             s[i] += s[i -1];//更新前缀和

for (int len = 2; len <= n;len ++)//枚举区间长度len,长度为1时,不用合并代价是0

for (int i =1; i + len -1<= n; i++)//枚举区间左端点i

{

int j = i + len - 1;//右端点j-i+1个元素

f[i][j]= 1e8;.//初始化为一个较大的值

for (int k = i; k< j;k ++)// 枚举k,最后一次合并

f[i][j]=min(f[i][j], f[i][k]+f[k +1][j]+s[j] -s[i -1]);// f[i][k]左边石子重量,f[k +1][j]右边石子重量,s[j] -s[i -1])最后一次合并的代价/即从第i堆到第j堆石子的总质量即前缀和

}//状态转移,时间复杂度状态数量n2,k的枚举n,总共O(n3)

printf(“%d”, f[1][n]);

    return 0;

}

900.整数划分

解1由完全背包问题推论出:

状态转移方程:

-->f[i][j]=f[i-1][j]+f[i][j-i]

#include <stdio.h>

const int N=1010;

int n;

int mod=1e9+7;

int f[N][N];//优化为f[N];

int main()

{

    scantf("%d",&n);

    pintf("%d",n);

    f[0]=1;//初始化只有一个数时

    for(int i=1;i<=n;i++)

        for(int j=i;j<=n;j++)

            f[i][j]=(f[i-1][j]+f[i][j-i])%mod;//优化为一维数组f[j]=f[j]+f[j-i];-->f[j]=f[j]+f[j-i])%mod;

    printf("%d",f[n]);

    return 0;

}

第六讲.贪心法:每次选择局部最优解

905区间选点

  • 将每个区间按右端点从小到大排序
  • 从前往后依次枚举每个区间

        如果当前区间中已经包含点,则跳过,

        否则,选择当前区间右端点

代码实现:

#include <stdio.h>

const int N=10010;

int a[N],b[N],f[N];

int n;

int main()

{

    scantf("%d",&n);

    for(int i=1;i<=n;i++)

        scantf("%d%d",a[i],b[i])

    int j=1;

    f[j]=b[1];

    for(int i=1;i<=n;i++)

    {

        if(b[j]<a[i])

        b[++j]=b[i];

    }

    printf("%d",j);

    return 0;

}

906区间分组

  • 将所有区间按左端点从小到大排序à待排序区间的左端点一定比现有组区间左端点大
  • 从前往后处理每个区间,判断能否将其放入某个现有区间组中(左端点是否<=当前区间组中的右端点的最大

判断条件:L[i]>Max_r//L[i]待排区间左端点,Max_r当前区间最大右端点

如果不存在这样的组,则开一个新组,将其放进去

如果存在这样的组,选其中一个将它放入吧组中并更新当前组的Max_r

贪心证明思路:①证Ans<=cnt ②证Ans>=cntèans==cnt

//ans:最终答案   cnt:按照相应算法得到的一个组的数量

代码实现:

#include <stdio.h>
const int N =100010;
#include <queue>;//用到小根堆include queue
int n;
struct Range
{
int l, r;
bool operator< (const Range &W)const;
{
return 1 < W.l;
}
}
range[N];
int main()
{
scanf(“%d”,&n);
for (int i = 0; i< n; i ++)
{
int l, r;
scanf(“%d%d”,&1, &r);
range[i]=(1, r);
}
sort(range, range + n);
priority_queue<int, vector<int>, greater<int>> heap; //所有组的右端点的最大值,从大到小排序
for (int i = 0; i <n; i ++)
{
auto r = range[i];//r表示当前区间
if (heap.empty() || heap.top() >= r.l) heap.push(r);
/* heap.empty()表堆空
heap.top() >= r.l表堆顶大于当前区间左端点
heap.push(r)表将当前区间调入到一个新的组中去*/
else
{
	int t=heap.top();//将其放到最小值的组中去
	heap.pop();//删掉最小值堆顶
	heap.push(r,r);//新的有端点加入到堆中去
}
}
printf(“%d\n”,heap.size());
return 0;
}

338计数问题:

数位统计问题重要方法:分情况讨论

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值