第一讲。语法基础
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)时用一个滚动数组,只需保留上一层结果,之前结果不会再用到,可删掉
#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计数问题:
数位统计问题重要方法:分情况讨论