本周完成了以下几个任务:
1.搜索题专项练习
2.树的存储、查找、删除、添加、遍历
3.cf思维题结构题练习
4.kmp算法、拓扑排序的了解与实现
先来总结一下我遇到的搜索题的题型
1.连通块问题:
[USACO10OCT]Lake Counting S - 洛谷
此类题型我一般用bfs+坐标偏移量,再根据题目要求判断是否进行入队即可。
2.01背包问题
[USACO2.1]健康的荷斯坦奶牛 Healthy Holsteins - 洛谷
dfs寻求最优解,对每个状态进行递归选与不选两种状态+回溯标记。答案数组的存储、输出。
3.最短路问题
此类题型一般是模拟+bfs,根据题意进行剪枝优化,在两方面进行优化:根据当前状态选择是否入队,哪几种方法入队,哪几种不入;每次循环先进行判断,不符合题意的直接跳过一轮(该判断与最终的结束条件不是同一个)
再来说一下这周做的部分搜索题的体会
[USACO2.1]健康的荷斯坦奶牛 Healthy Holsteins - 洛谷
每种饲料选与不选两种状态,经典dfs。
难点:数据存储,判断,递归理解。
先说数据存储:需要定义一个随条件判断更新的答案数组来存储用到了哪个编号的饲料,同时牵扯出来的问题:需要定义一个数组存储每次搜索选的饲料的编号,把它传给答案数组,而这个存编号的数组需要在递归时更新与回溯。
再来看判断:判断已选择的每种饲料总和是否分别大于最小量,牵扯出来一个问题,如何将这个总和表示为已选择的每种饲料的总和,如 选择1,3号未选2号,因此不能用简单的从1到n累加。代码如下:值得仔细体会
for(int i=1; i<=n; i++)
{
int sum=0;
for(int j=1; j<=x; j++)
sum+=b[c[j]][i];//划重点!!!!!!
if(sum<a[i]) return false;
}
return true;
最后再来谈递归:
void dfs(int t,int s)
//t代表第t号饲料
//s代表当前已经选择的饲料的种数
c[s+1]=t;//c数组存储每次选的饲料的编号,t号选入
dfs(t+1,s+1);//选择t号,再继续选t+1号
c[s+1]=0;//注意这个回溯的理解!!!!!
dfs(t+1,s);//t号不选入,再继续选t+1号
虽然说一道题花了一个小时,但是收获很大。要摆脱功利性思维,并不是一晚上要搞懂多少题,写多长的博客,得到什么等级的评价,这些都不是自己的东西,只有收获到的新思路,新题型,新方法,新思维,才是自己的东西。
搜索每个状态,dfs
难点:如何搜索所有数据,递归传参
按行搜,一行搜完(当前行数+1>col)则行数重新赋为1,列数+1.
递归判断,八个偏移量均未标记则标记当前坐标,递归搜索下一个点,并让sum加上当前坐标的数据,因此递归需要传两个内容:坐标和sum。回溯:取消标记。
两大类情况:1.八个坐标未标记:标记,递归搜下一个点,回溯。2.八个坐标至少有一个已标记,该点不选,递归搜下一个(表示为sum直接传到下一个点,不加当前坐标的数据)
经典dfs的全排列
总结模板:
1.考虑递归需要(必须传入的改变量)传几个参数
2.设置flag标记数组,已搜过的不再搜。满足条件进行标记,递归下一个状态,回溯时取消标记
3.设置递归返回条件,如到达边界,搜完全部,不符合条件等
4.数据(答案数组)的存储与表示。
如全排列中
void dfs(int m){
...
...
for (int i = 1; i <= n; i++) {
if (!flag[i]) {
store[m] = i;//注意这个m的意思
flag[i] = true;
dfs(m+1);
flag[i] = false;
}
}
}
或者荷斯坦奶牛中的
void dfs(int t,int s)//t代表第t号饲料,s代表当前已经选择的饲料的种数
{ ...
...
c[s+1]=t;//c数组存储选的饲料的编号,注意这个t和s+1的意思
dfs(t+1,s+1);
c[s+1]=0;
dfs(t+1,s);
}
[USACO06OCT] Cows on Skates G - 洛谷
关于答案数组的存储还有这个题
while(!q.empty()){
int x=q.front().first;
int y=q.front().second;
q.pop();
for(int i=1;i<=4;i++){
int xx=x+fx[i];
int yy=y+fy[i];
if(illegal(xx,yy)) continue;
dist[xx][yy][0]=x;//答案数组的存储
dist[xx][yy][1]=y;
vis[xx][yy]=true;
q.push(std::make_pair(xx,yy));
if(xx==r&&yy==c) break;
}
}
答案数组输出(暂时没有理解,后期解决会进行更新)
void WriteWay(int x,int y){
if(!(dist[x][y][0]+dist[x][y][1])) return;
//如果数组中为0,表示到头了,return
WriteWay(dist[x][y][0],dist[x][y][1]);
cout<<x<<" "<<y<<endl;//递归输出路径
}
树的存储与基本操作在这里就不多说了,感觉目前接触的主要还是递归的理解和应用,没有做题,没有自己的感悟,只有知识点,就不进行罗列了。
cf题做的比较少,浅谈一下印象最深的吧
关于下标越界的感悟
for (int i = 0; i + 1 < a.size(); i++) {
if (a[i + 1] - a[i] == 1) ans += 2;
else if (a[i + 1] - a[i] == 2) ans += 1;
}
遇到需要用到下标为i+1这种可能会越界的表示方法,在条件中写为i+1<n即可解决大部分情况。
(之前一直写i<n-1的形式,仍存在越界可能,直接i+1<n完事)
kmp算法体会
主要是根据最长相等前后缀让子串移动时不至于每一步都回溯,用空间换时间。
拓扑排序体会
有向无环图,即拓扑图,根据度数来进行排序
模板:
queue<-入队所有度数为0的点
while(q非空){
t存储队头
枚举t的所有出边t->j
删除t->j,d[j]--;
if(d[j]==0) queue<-j;
}
如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
涉及树与图的存储的部分下周继续看资料