行亦谦ACM自闭之旅第三周
DFS
提示:DFS最重要的是顺序,回溯就是恢复现场, 什么时候进入下一层递归,修改状态,什么时候从递归出来恢复状态,剪枝 提前判断,不符合条件,停止进入下一层,直接回溯
优先考虑深度,换句话说就是一条路走到黑,直到无路可走的情况下,才会选择回头,然后重新选择一条路。
BFS
BFS(广度优先搜索,也可称宽度优先搜索)是连通图的一种遍历策略。因为它的基本思想是从一个顶点V0开始,辐射状地优先遍历其周围较广的区域。
提示:适用题目最短距离,最少操作几次
广度优先搜索(BFS)类似于二叉树的层序遍历算法,它的基本思想是:首先访问起始顶点v,然后由v出发,依次访问v的各个未被访问过的邻接顶点w1,w2,w3….wn,然后再依次访问w1,w2,…,wi的所有未被访问过的邻接顶点,再从这些访问过的顶点出发,再访问它们所有未被访问过的邻接顶点….以此类推,直到途中所有的顶点都被访问过为止。类似的想法还将应用与Dijkstra单源最短路径算法和Prim最小生成树算法。
广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批顶点,不像深度优先搜索(DFS)那样有回退的情况,因此它不是一个递归的算法,为了实现逐层的访问,算法必须借助一个辅助队列并且以非递归的形式来实现。
BFS搜索步骤
1、首先创建一个visit[ ]数组和一个队列q,分别用来判断该位置是否已经访问过及让未访问过的点入队;
2、初始化visit[ ]数组,清空q队列;
3、让起点start入队,并使该点的visit置1;
4、while(!q.empty()){…}执行搜索操作,
a、取出队头元素后使队头元素出队,判断该元素是否为目标到达点;
b、如果是目标点,就返回结果(一般是最短时间、最短路径);
c、如果不是目标点,就继续访问与其相邻的位置点,将可走的相邻的位置点入队,并更新visit[ ]数组;
回溯算法
回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。
回溯算法模板
理念:for循环横向遍历,递归纵向遍历,回溯不断调整结果集
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
注:优化回溯算法只有剪枝一种方法,在回溯算法:组合问题再剪剪枝中把回溯法代码做了剪枝优化
剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了
回溯算法能解决如下问题:
组合问题:N个数里面按一定规则找出k个数的集合
排列问题:N个数按一定规则全排列,有几种排列方式
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
棋盘问题:N皇后,解数独等等
贪心算法
贪心法求解的问题的特征:
(1)最优子结构性质
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质,也称此问题满足最优性原理。
(2)贪心选择性质
所谓贪心选择性质是指问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来得到。
贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
解题步骤
1.建立数学模型来描述问题;
2.把求解的问题分成若干个子问题;
3.对每一子问题求解,得到子问题的局部最优解;
4.把子问题的局部最优解合成原来问题的一个解
分治思想
分治(divide-and-conquer)就是“分而治之”的意思,其实质就是将原问题分成n个规模较小而结构与原问题相似的子问题;然后递归地解这些子问题,最后合并其结果就得到原问题的解。当n=2时的分治法又称二分法。
用分治法建立模型时,解题步骤分为三步:
(1)分解(Divide):把问题分解成独立的子问题;
(2)解决(Conquer):递归解决子问题;
(3)合并(Combine):把子问题的结果合并成原问题的解。
分治法的题目,需要符合两个特征
(1)平衡子问题:子问题的规模大致相同。能把问题划分成大小差不多相等的k个子问题,最好k=2,即分成两个规模相等的子问题。子问题规模相等的处理效率,比子问题规模不等的处理效率要高。
(2)独立子问题:子问题之间相互独立。这是区别于动态规划算法的根本特征,在动态规划算法中,子问题是相互联系的,而不是相互独立的。
理念:分治法的经典应用,有二分查找、归并排序、快速排序等。
快速排序算法模板-AcWing
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j)
{
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}
归并排序算法模板-AcWing
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 k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
else tmp[k ++ ] = q[j ++ ];
while (i <= mid) tmp[k ++ ] = q[i ++ ];
while (j <= r) tmp[k ++ ] = q[j ++ ];
for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}
整数二分算法模板 -AcWing
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
浮点数二分算法模板 -AcWing
bool check(double x) {/* ... */} // 检查x是否满足某种性质
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
位运算
计算机中的数在内存中都是以二进制形式进行存储的 ,而位运算就是直接对整数在内存中的二进制位进行操作,因此其执行效率非常高,在程序中尽量使用位运算进行操作,这会大大提高程序的性能。
位运算基础
&
按位与
如果两个相应的二进制位都为1,则该位的结果值为1,否则为0
|
按位或
两个相应的二进制位中只要有一个为1,该位的结果值为1
^
按位异或
若参加运算的两个二进制位值相同则为0,否则为1
~
取反
~是一元运算符,用来对一个二进制数按位取反,即将0变1,将1
<<
左移
用来将一个数的各二进制位全部左移N位,右补0
>>
右移
将一个数的各二进制位右移N位,移到右端的低位被舍弃,对于无符号数, 高位补0
负数的位运算
首先,我们要知道,在计算机中,运算是使用的二进制补码,而正数的补码是它本身,负数的补码则是符号位不变,其余按位取反,最后再+ 1 +1+1得到的, 例如:
15 1515,原码:00001111 00001111\space00001111 补码:00001111 0000111100001111
− 15 -15−15,原码:10001111 10001111\space10001111 补码:11110001 1111000111110001
那么对于负数的位运算而言,它们的操作都是建立在补码上的,得到的运算结果是补码,最后将补码结果转化成一个普通的十进制数结果。但需要注意的是,符号位是需要参与运算的,而在左移右移操作中,负数右移补1 11,左移右边补0 00。例如对于− 15 -15−15,其补码为11110001 , 11110001,11110001,右移一位( − 15 > > 1 ) (-15>>1)(−15>>1)得到的是11111000 1111100011111000,即− 8 -8−8,其他的同理。
这里我们介绍几个特殊的性质:
快速判断是否为− 1 -1−1
在链式前向星中,我们初始化h e a d headhead数组为− 1 -1−1,最后判断是否遍历完u uu的所有边时,即判断i ii是否为− 1 -1−1,我们直接用∼ i \sim i∼i即可。原因就在于− 1 -1−1的补码是11111111 1111111111111111,按位取反就变为00000000 0000000000000000,这实际上就是0 00。
取最低位的1 11,lowbit函数
也就是x & ( − x ) x\&(-x)x&(−x),这在树状数组中起着巨大作用,这里指路一篇树状数组讲解b l o g blogblog:点这里,我们来证明一下,这里取x = 15 x=15x=15,对于15 & ( − 15 ) 15\&(-15)15&(−15),我们知道,在补码上进行运算得到的是00000001 0000000100000001,需要注意二元运算的符号位我们需要进行运算。
位运算的应用
位运算实现乘除法
将x左移一位实现× 2 ,将x xx右移一位实现÷2。
a < < 1 ≡ a ∗ 2
a > > 1 ≡ a / 2
位运算交换两整数
void swap(int &a,int &b){
a ^= b;
b ^= a;
a ^= b;
}
例题
A - Red and Black
题目描述
There is a rectangular room, covered with square tiles. Each tile is colored either red or black. A man is standing on a black tile. From a tile, he can move to one of four adjacent tiles. But he can't move on red tiles, he can move only on black tiles.
Write a program to count the number of black tiles which he can reach by repeating the moves described above.
有一个长方形的房间,上面铺满了方形瓷砖。每块瓦片的颜色不是红色就是黑色。一个人站在一块黑色的牌上。他可以从一块牌上移动到相邻的四块牌中的一块。但他不能在红砖上移动,只能在黑砖上移动。
写一个程序来计算他通过重复上述的移动可以到达的黑色牌的数量。
Input
The input consists of multiple data sets. A data set starts with a line containing two positive integers W and H; W and H are the numbers of tiles in the x- and y- directions, respectively. W and H are not more than 20.
There are H more lines in the data set, each of which includes W characters. Each character represents the color of a tile as follows.
'.' - a black tile
'#' - a red tile
'@' - a man on a black tile(appears exactly once in a data set)
输入由多个数据集组成。一个数据集从包含两个正整数W和H的行开始;W和H分别是x-和y-方向上的瓷砖数量。W和H不超过20。
数据集中还有H行,每行包括W个字符。每个字符代表一个瓦片的颜色,如下所示。
'.' - 一个黑色的瓦片
'#' - 一个红色的瓷砖
'@' - 黑色瓷砖上的一个人(在数据集中正好出现一次)
Output
For each data set, your program should output a line which contains the number of tiles he can reach from the initial tile (including itself).
对于每一个数据集,你的程序应该输出一行,其中包含他从初始瓦片(包括其本身)可以到达的瓦片数量。
Sample
Input
6 9
....#.
.....#
......
......
......
......
......
#@...#
.#..#.
11 9
.#.........
.#.#######.
.#.#.....#.
.#.#.###.#.
.#.#..@#.#.
.#.#####.#.
.#.......#.
.#########.
...........
11 6
..#..#..#..
..#..#..#..
..#..#..###
..#..#..#@.
..#..#..#..
..#..#..#..
7 7
..#.#..
..#.#..
###.###
...@...
###.###
..#.#..
..#.#..
0 0
Output
45
59
6
13
AC代码
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
char room[23][23];
int dir[4][3]={
{ -1, 0},
{ 0, -1},
{ 1, 0},
{ 0, 1},
};
int Wx, Hy, num;
#define CHECK(x, y)(x<Wx && x>=0 && y>=0 && y<Hy)
struct node { int x, y;};
void BFS( int dx, int dy){
num = 1;
queue <node>q;
node start , next;
start.x = dx;
start.y = dy;
q.push(start);
while ( !q.empty()){
start = q.front();
q.pop();
for(int i =0; i<4; i++){
next.x = start.x + dir[i][0];
next.y = start.y + dir[i][1];
if(CHECK(next.x, next.y)&& room[next.x][next.y]=='.'){
room[next.x][next.y]='#';
num++;
q.push(next);
}
}
}
}
int main()
{
int x, y, dx, dy;
while( cin >> Wx >> Hy){
if(Wx==0 && Hy==0)
break;
for( y=0; y<Hy; y++){
for( x=0; x<Wx; x++){
cin >> room[x][y];
if(room[x][y] == '@'){
dx = x;
dy = y;
}
}
}
num = 0;
BFS(dx, dy);
cout << num << endl;
}
return 0;
}
B - N皇后问题
题目描述
在N*N的方格棋盘放置了N个皇后,使得它们不相互攻击(即任意2个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成45角的斜线上。
你的任务是,对于给定的N,求出有多少种合法的放置方法。
Input
共有若干行,每行一个正整数N≤10,表示棋盘和皇后的数量;如果N=0,表示结束。
Output
共有若干行,每行一个正整数,表示对应输入行的皇后的不同放置数量。
Sample
Input
1
8
5
0
Output
1
92
10
AC代码
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
int n, tot = 0;
int col[12] = {0};
bool check( int c, int r){
for(int i =0; i<r; i++)
if( col[i] == c|| (abs(col[i] -c)) == abs(i-r))
return false;
return true;
}
void DFS( int r){
if( r== n){
tot++;
return ;
}
for(int c=0; c<n; c++)
if(check( c, r)){
col[r] = c;
DFS(r+1);
}
}
int main(){
int ans[12]={0};
for( n =0; n<=10; n++){
memset(col, 0, sizeof(col));
tot = 0;
DFS(0);
ans[n] = tot;
}
while( cin >> n){
if( n == 0)
return 0;
cout << ans[n]<< endl;
}
return 0;
}
C-蒜头君的生日 (基姆拉尔森计算公式)
蒜头君的生日快到了,蒜头君希望是在周末,蒜头君请你帮忙算出他生日在星期几。
Input
输入三个正整数,分别表示年、月、日。保证输入年份合法。
Output
输出星期几。用 Monday、Tuesday、Wednesday、Thursday、Friday、Saturday、Sunday 表示星期几。
Sample Input
1 1 1
Sample Output
Monday
基姆拉尔森计算公式
W= (d+2*m+3*(m+1)/5+y+y/4-y/100+y/400+1)%7
在公式中d表示日期中的日数,m表示月份数,y表示年数。
注意:在公式中有个与其他公式不同的地方:
把一月和二月看成是上一年的十三月和十四月
例:如果是2004-1-10则换算成:2003-13-10来代入公式计算。
AC代码
#include <iostream>
#include <string>
using namespace std;
int whatday(int y, int m, int d) {
// 返回正确的星期。用 0 - 6 表示 星期 1 - 7
// 注意每年的 1 月 2 月要当上年的 13 月、14 月计算。
if(m==1 || m==2){
m+=12; y--;
}
return (d+2*m+3*(m+1)/5+y+y/4-y/100+y/400)%7; //基姆拉尔森计算公式
}
string weekday[7] = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};
int main() {
int y, m, d;
cin >> y >> m >> d;
cout << weekday[whatday(y, m, d)] << endl;
return 0;
}
D-恋爱纪念日
题目描述
蒜头君和花椰妹谈恋爱啦。祝福他们吧。蒜头君想知道第他们的第100天,200天...纪念日。
Input
输入4个整数y,m,d,k,表示他们在一起的日期,保证是一个1900年1月1日以后的日期,蒜头君想知道他们的k(0≤k≤10000)天纪念日。
Output
输出格式按照yyyy-mm-dd的格式输出k天纪念日的日期。月份和天数必须各输出2位。保证最后答案年份不超过4位。
Sample Input
2016 10 1 100
Sample Output
2017-01-09
AC代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <stack>
using namespace std;
int is_leap_year(int year) { //判断是否为闰年,是:返回1; 不是:返回0.
if (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)) {
return 1;
}
return 0;
}
int main() {
int y,m,d,n,x;
int a[]={0,31,28,31,30,31,30,31,31,30,31,30,31};
cin>>y>>m>>d>>n;
if(is_leap_year(y)&&m==2)x=29-d;
else x=a[m]-d;
if(x>=n)d=d+n;
else{
n-=x;
if(is_leap_year(y)&&m==2)d=29;
else d=a[m]; m++; int flag=0;
for(;;m++) {
if(m>=13){
m-=12;y++;
}
if(is_leap_year(y)&&m==2)n-=29;
else n-=a[m];
if(n<0) {
if(is_leap_year(y)&&m==2)n+=29;
else n+=a[m]; d=n; flag=1;
}
else if(n==0) {
if(is_leap_year(y)&&m==2)d=29;
else d=a[m]; flag=1;
}
if(flag)break;
}
}
printf("%04d-%02d-%02d\n",y,m,d); return 0;
}
E-解方程
题目描述
求方程f(x)=2^x+3^x-4^x=0在[1,2]内的根。
Input
输入m(0<=m<=8),控制输出精度
Output
输出方程f(x)=0的根,x的值精确小数点m位
Sample Input
3
Sample Output
1.507
提示:提示:2^x可以表示成exp(x*ln(2))的形式。
#include<bits/stdc++.h>
using namespace std;
const int N = 5e6 + 10
const double eps = 1e-10; // 控制误差
typedef long long LL;
double f(double x)
{
return powl(2, x) + powl(3, x) - powl(4, x);
}
int main()
{
int m;
cin >> m;
cout.precision(m), cout.setf(ios::fixed); // 小数点后输出个数
double l = 1, r = 2;
while (r - l > eps)
{
double mid = (l + r) / 2;
if(f(mid) > 0) l = mid;
else r = mid;
cout << l << '\n';
}
总结
想得太多做的太少
变量控制输出精度
1.直接使用转换说明符
我们都知道可以通过%m.n来控制数据的输出范围,这里要求m和n都是字面量
类似地,标准库也提供了通过变量来控制宽度的方法,这时我们就要使用%.。当使用*的时候要求通过一个int类型的参数来指定宽度,如果参数是正数则输出右对齐,如果参数是负数则输出内容左对齐,相当于添加了一个-标签。例子如下:
int w1, w2;
float a, b;
scanf("%f%f", &a, &b);
scanf("%d%d", &w1, &w2);
printf("%*.*f", w1, w2, a / b);//此时a/b的显示宽度由w1和w2来控制
2.预先构造一个格式化字符串
通常格式字符串我们都使用一个字符串字面量,这样就没办法把一个变量放到格式说明符中。事实上这个参数要求的类型是const char *,因此我们完全可以预先构造一个字符串,将用户输入的指定宽度的变量格式化到这个字符串里面就好了,同样是上面的功能可以写成:
int w1, w2;
float a, b;
char format[10];
scanf("%f%f", &a, &b);
scanf("%d%d", &w1, &w2);
fprintf(format, "%%%d.%df", w1, w2);
printf(format, a/b);
3.C++
cout.precision(m), cout.setf(ios::fixed); // 小数点后输出个数