入门
Ants
这道题是后面说到的弹性碰撞问题,可以视为互相穿过运动。
Physics Experiment(158页)
高中的物理公式也很重要
对于复杂的问题我们可以考虑有两个球的情况,和蚂蚁问题类似的,两球相撞以后他们就各自弹开,相当于继续按照原来的轨迹运动。
抽签问题(二分+折半查找)O(n2logn)
本题主要在于一个折半查找的预处理
k
c
_{c}
c+k
d
_{d}
d=m-k
a
_{a}
a-k
b
_{b}
b
我们hash一下k
c
_{c}
c+k
d
_{d}
d的值并进行排序,再用二重循环列举ka+kb,对于每一个k
a
_{a}
a+k
b
_{b}
b的值,二分查找它对应的k
c
_{c}
c+k
d
_{d}
d.
这里的哈希方法我们使用开放定址法。
H
i
_{i}
i=(H(key) + d
i
_{i}
i)
由于每个数最大只能是n,H
i
_{i}
i=c*n+d绝对不会冲突
bool binary_search(int x)
{
int l=0,r=n*n;
while(l<r)
{
int mid=l+r>>1;
if(h[mid]==x) return true;//在哈希表里有,找到了
else if(hash[mid]<x) l=mid+1;
else r=mid;
}
return false;
}
void solve()
{
for(int c=0;c<n;c++)
{
for(int d=0;d<n;d++)
{
h[c*n+d]=k[c]+k[d];
}
}
sort(h,h+n*n);
for(int a=0;a<n;a++)
{
for(int b=0;b<n;b++)
{
if(binary_search(m-k[a]-k[b]))
{
f=true;
}
}
}
}
4 values whose sum is 0(P160)
这道题其实就是抽签那道题记录方案数的做法,我们用一个res来记录方案数。
res+=upper_bound(h,h+n* n,m-k[a]-k[b])-lower_bound(h,h+n* n,m-k[a]-k[b])
用upper_bound减去lower_bound得到等于x的数的个数是很常用的手法。
超大背包问题(P162)
这题是个数据很大的01背包问题,直接循环必定TLE。我们将数据分为两部分处理:
第一部分预处理前半部分数据二进制枚举出的情况。
将其排序,并筛选性价比高的结果。
同样二进制枚举后半部分,找出前半部分中w
1
_{1}
1<=W-w
2
_{2}
2的v的最大值。
将后半部分枚举完就可以找出最终的最大值。
这道题将在背包问题优化的专题详细叙述。
最基础的"穷竭搜索"
DFS在每个阶段枚举他的不同分支后回溯,总的转态都是一颗搜索树,每个状态相当于先入栈,再让他的子状态入栈,直到子状态都出栈他才出栈。
部分和问题
可以作为dfs的经典模板,设计dfs参数,考虑有什么边界状态,考虑有几个分支。
对于此题提问的是否能选出若干数,返回值就设计为bool类型;对于记忆化搜索,返回值可以为一个数;否则,一般来说我们用状态记录值,不用返回值记录值。
bool dfs(int sum,int i)
{
if(i==n) return sum==k;
if(dfs(sum,i+1)) return true;
if(dfs(sum+a[i],i+1)) return true;
return false;
}
Lake Counting
DFS的经典联通块问题
注意如果是有遍历顺序的题,dx和dy数组的各个顺序也要有变化。
bool dfs(int x,int y)
{
mapp[x][y]='.';
for(int i=0;i<4;i++)
{
int nx=x+dx[i];
int ny=y+dy[i];
if(0<=nx&&nx<N&&0<=ny&&ny<M&&mapp[nx][ny]=='W')
dfs(nx,ny);
}
return;
}
void solve()
{
int res=0;
for(int i=0;i<N;i++){
for(int j=0;j<M;j++)
{
if(mapp[i][j]=='W')
{
dfs(i,j);
res++;
}
}
}
cout<<res;
}
迷宫的最短路径
BFS用来解决不带权的图中最短路径。
用队列保存状态即可,一般迷宫问题用pair<x,y>保存状态,保存的状态比较复杂的时候,我们可以将他编写成结构体。
BFS模板:
q.push(start);
while(!q.empty())
{
u=q.front();
q.pop();
for(int i=h[u];i!=-1;i=ne[i])
{
int v=e[i];
if(!vis[v])
{
vis[v]=1;
q.push(v);
//干你想做的事
}
}
}
BFS求最短距离
,我们用一个数组d[m][n]将最短距离保存下来,初始化为INF。这个数组也可以当成一个visit数组,用于标记已经访问过的结点。
int bfs()
{
queue<P> q;
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
d[i][j]=0x3f;
}
}
q.push(P(sx,sy));
d[sx][sy]=0;
while(!q.empty())
{
P p=q.front();
q.pop();
if(p.frist==endx&&p.second==endy) break;
for(int i=0;i<4;i++)
{
int nx=p.first+dx[i];
int ny=p.second+dy[i];
if(0<=nx&&nx<N&&0<=ny&&ny<=M&&maze[nx][ny]!='#'&&d[nx][ny]=0x3f)
{
q.push(P(nx,ny));
d[nx][ny]=d[p.first][p.second]+1;
}
}
}
return d[endx][endy];
}
BFS求最短方案
,我们开数组记录每个点由什么操作转换(或者记录前一个二维坐标),之后再沿着这个路径找回去,再反转一下就是整个方案了。
蓝桥杯第十届迷宫那题:
void bfs()
{
queue<P> q;
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
d[i][j]='0';
}
}
q.push(P(sx,sy));
d[sx][sy]='1';
while(!q.empty())
{
P p=q.front();
q.pop();
if(p.frist==endx&&p.second==endy) break;
int nx=p.first+dx[0];
int ny=p.second+dy[0];
if(0<=nx&&nx<N&&0<=ny&&ny<=M&&maze[nx][ny]!='#'&&d[nx][ny]='0')
{
q.push(P(nx,ny));
d[nx][ny]='D';
}
int nx=p.first+dx[1];
int ny=p.second+dy[1];
if(0<=nx&&nx<N&&0<=ny&&ny<=M&&maze[nx][ny]!='#'&&d[nx][ny]='0')
{
q.push(P(nx,ny));
d[nx][ny]='L';
}
int nx=p.first+dx[2];
int ny=p.second+dy[2];
if(0<=nx&&nx<N&&0<=ny&&ny<=M&&maze[nx][ny]!='#'&&d[nx][ny]='0')
{
q.push(P(nx,ny));
d[nx][ny]='R';
}
int nx=p.first+dx[3];
int ny=p.second+dy[3];
if(0<=nx&&nx<N&&0<=ny&&ny<=M&&maze[nx][ny]!='#'&&d[nx][ny]='0')
{
q.push(P(nx,ny));
d[nx][ny]='U';
}
}
x=endx;
y=endy;
while(!(x=sx&&y=sy))
{
res+=d[x][y];
if(d[x][y]=='D')
{
x-=dx[0];
y-=dy[0];
}
else if(d[x][y]=='U')
{
x-=dx[1];
y-=dy[1];
}
else if(d[x][y]=='L')
{
x-=dx[2];
y-=dy[2];
}
else
{
x-=dx[3];
y-=dy[3];
}
}
res.reverse();
}
关于剪枝
最优性,可行性,重复性,奇偶性,启发式。
可行性剪枝
#include <iostream>
using namespace std;
int n, k, sum, ans;
int a[40];
void dfs(int i, int cnt, int s) {
if(cnt>k)
{
return;
}
if(s>sum){
return;
}
if (i == n) {
if (cnt == k && s == sum) {
ans++;
}
return;
}
dfs(i + 1, cnt, s);
dfs(i + 1, cnt + 1, s + a[i]);
}
int main() {
n = 30;
k = 8;
sum = 200;
for (int i = 0; i < 30; i++) {
a[i] = i + 1;
}
ans = 0;
dfs(0, 0, 0);
cout << ans << endl;
return 0;
}
重复性剪枝
选过了数i,下次我们就从i+1开始选了,避免了重复。
这个例子不太好,以后看有什么好的吧。
#include <iostream>
using namespace std;
int n, k, sum, ans;
int a[40];
bool xuan[40];
void dfs(int s, int cnt,int pos) {
if(s>sum||cnt>k)
{
return;
}
if (s == sum && cnt == k) {
ans++;
}
for (int i = pos; i < n; i++) {
if (!xuan[i]) {
xuan[i] = 1;
dfs(s + a[i], cnt + 1,i+1);
xuan[i] = 0;
}
}
}
int main() {
n = 30;
k = 8;
sum = 200;
for (int i = 0; i < 30; i++) {
a[i] = i + 1;
}
ans = 0;
dfs(0, 0,0);
cout << ans << endl;
return 0;
}
奇偶性剪枝
把地图分割成黑方格和白方格,如果奇偶性不对就没必要再做下去了。
#include <iostream>
using namespace std;
const int N = 10;
int n, m, T;
char mat[N][N];
bool vis[N][N];
int dx[4]={0,0,-1,1};
int dy[4]={1,-1,0,0};
bool ok;
void dfs(int x,int y,int t)
{
if(ok) return;
if(t==T){
if(mat[x][y]=='D')
ok=true;
return;
}
vis[x][y]=true;
for(int i=0;i<4;i++)
{
int tx=x+dx[i];
int ty=y+dy[i];
if(tx<0||tx>=n||ty<0||ty>=m||mat[tx][ty]=='X'||vis[tx][ty])
continue;
dfs(tx,ty,t+1);
}
vis[x][y]=false;
}
int main() {
cin >> n >> m >> T;
for (int i = 0; i < n; ++i) {
cin >> mat[i];
}
int sx,sy,ex,ey;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(mat[i][j]=='S')
sx=i,sy=j;
if(mat[i][j]=='D')
ex=i,ey=j;
}
}
if((sx+sy+ex+ey+T)%2!=0)
{
cout<<"NO"<<endl;
}
else{
ok=false;
dfs(sx,sy,0);
if(ok)
cout<<"YES"<<endl;
else
cout<<"NO"<<endl;
}
return 0;
}
例题:引爆炸弹
弱智题
#include <stdio.h>
#include <iostream>
using namespace std;
char mat[1010][1010];
int n, m;
bool row[1010],col[1010];
void boom(int x,int y)
{
mat[x][y]=0;
if(!row[x]){
row[x]=true;
for(int i=0;i<m;++i){
if(mat[x][i]=='1')
{
boom(x,i);
}
}
}
if(!col[y])
{
col[y]=true;
for(int i=0;i<n;++i){
if(mat[i][y]=='1'){
boom(i,y);
}
}
}
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; ++i) {
scanf("%s", mat[i]);
}
int cnt=0;
for(int i=0;i<n;++i)
{
for(int j=0;j<m;++j){
if(mat[i][j]=='1'){
++cnt;
boom(i,j);
}
}
}
cout<<cnt<<endl;
return 0;
}
例题:生日蛋糕
这道题很重要,要推导很多的数学公式
另:求最大最小值的时候可以用搜索,运用全局变量ans在合适的时候进行比较即可。
我们先要考虑R和H的枚举范围
当前情况体积太大:可以进行可行性剪枝。
最优性剪枝需要经历一个放缩。
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <iostream>
using namespace std;
const int INF = 0x3f3f3f3f;
int n, m;
int ans;
int va[20];
void dfs(int u,int v,int s,int r0,int h0)
{
if(u==m)
{
if(v==n){
ans=min(ans,s);
}
return;
}
if(va[m-u]+v>n)
return;
if(2.0*(n-v)/r0+s>ans)
return;
for(int r=r0;r>=m-u;r--)
{
for(int h=h0;h>=m-u;h--)
{
int tv=v+r*r*h;
if(tv>n)
continue;
int ts=s+2*r*h;
if(u==0)
ts+=r*r;
dfs(u+1,tv,ts,r-1,h-1);
}
}
}
int main() {
cin >> n >> m;
for(int i=1;i<=m;i++){
va[i]=va[i-1]+i*i*i;
}
int r0=sqrt(n)+0.5;
ans=INF;
dfs(0,0,0,r0,n);
if(ans==INF)
ans=0;
cout<<ans<<endl;
return 0;
}
火柴棒
这道题中剪枝和贪心进行了结合
迭代加深
迭代加深的核心:
while(!dfs(depth)) depth++;
在dfs中失败return的条件是层数==depth
加成序列
#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int n;
int path[N];
bool dfs(int u,int depth)//depth用来控制迭代加深
{
if(u==depth) return path[u-1]==n;//判重
bool st[N]={false};
//从大到小枚举
for(int i=u-1;i>=0;i--)
for(int j=1;j>=0;j--)
{
int s=path[i]+path[j];
if(s==path[u-1]&&s<=n&&!st[s])
{
st[s]=true;
path[u]=s;
if(dfs(u-1,depth)) return true;
}
}
return false;
}
int main()
{
while(cin>>n,n)
{
int depth=1;
path[0]=1;
while(!dfs(1,depth)) depth++;//本层没有结果,一直下层
for(int i=0;i<depth;i++)
{
cout<<path[i]<<' ';
cout<<endl;
}
}
return 0;
}
另外BFS和DFS,在DFS深度过大,或者BFS空间过大的时候都可以互相代替,用DFS代替BFS有两种思路:
- 用状态记录最小步数
- 迭代加深
在导弹防御系统这题中,我们用dfs来搜索覆盖整个序列的最小单调序列数。
用状态记录最小步数法:
#include<bits/stdc++.h>
using namespace std;
int a[55];
int up[55],down[55];
int ans;
int n;
void dfs(int u,int su,int sd)
{
if(su+sd>=ans)//如果是n的话你的剪枝是不到位的
{
return;
}
if(u==n)
{
ans=su+sd;
return;
}
int k=0;
while(k<su&&up[k]>=a[u]) k++;
int t=up[k];
up[k]=a[u];
if(k<su)
dfs(u+1,su,sd);
else
dfs(u+1,su+1,sd);
up[k]=t;
k=0;
while(k<sd&&down[k]<=a[u]) k++;
t=down[k];
down[k]=a[u];
if(k<sd)
dfs(u+1,su,sd);
else
dfs(u+1,su,sd+1);
down[k]=t;
}
int main()
{
while(cin>>n,n)
{
ans=n;
for(int i=0;i<n;i++)
{
cin>>a[i];
}
dfs(0,0,0);
cout<<ans<<endl;
}
return 0;
}
用迭代加深法:
注意这边,只要找到第一个可以放的序列就可以结束了。
就可以证明不用开新序列就能把他放进去了。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 60;
int n;
int h[N];
int up[N], down[N];
bool dfs(int depth, int u, int su, int sd)
{
// 如果上升序列个数 + 下降序列个数 > 总个数是上限,则回溯
if (su + sd > depth) return false;
if (u == n) return true;
// 枚举放到上升子序列中的情况
bool flag = false;
for (int i = 1; i <= su; i ++ )
if (up[i] < h[u])
{
int t = up[i];
up[i] = h[u];
if (dfs(depth, u + 1, su, sd)) return true;
up[i] = t;
flag = true;
break; // 注意由上述证明的贪心原理,只要找到第一个可以放的序列,就可以结束循环了
}
if (!flag) // 如果不能放到任意一个序列后面,则单开一个新的序列
{
up[su + 1] = h[u];
if (dfs(depth, u + 1, su + 1, sd)) return true;
}
// 枚举放到下降子序列中的情况
flag = false;
for (int i = 1; i <= sd; i ++ )
if (down[i] > h[u])
{
int t = down[i];
down[i] = h[u];
if (dfs(depth, u + 1, su, sd)) return true;
down[i] = t;
flag = true;
break; // 注意由上述证明的贪心原理,只要找到第一个可以放的序列,就可以结束循环了
}
if (!flag) // 如果不能放到任意一个序列后面,则单开一个新的序列
{
down[sd + 1] = h[u];
if (dfs(depth, u + 1, su, sd + 1)) return true;
}
return false;
}
int main()
{
while (cin >> n, n)
{
for (int i = 0; i < n; i ++ ) cin >> h[i];
int depth = 0;
while (!dfs(depth, 0, 0, 0)) depth ++ ; // 迭代加深搜索
cout << depth << endl;
}
return 0;
}
启发式搜索
靶形数独
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int ones[N],log[N];
int row[N],col[N],cell[3][3];
int g[N][N];
int ans=-1;
inline int lowbit(int x)
{
return x&(-x);
}
void init()
{
for(int i=0;i<N;i++) log[1<<i]=i;
for(int i=0;i<N;i++)
for(int j=i;j;j-=lowbit(j))
ones[i]++;
for(int i=0;i<N;i++)
row[i]=col[i]=cell[i/3][i%3]=N-1;
}
inline int get_score(int x,int y)
{
return min(min(x,8-x),min(y,8-y))+6;
}
inline void draw(int x,int y,int t)
{
int s=1;
if(t>0) g[x][y]=t;
else
{
s=-1;
t=-t;
g[x][y]=0;
}
t--;
row[x]-=s<<t;
col[y]-=s<<t;
cell[x/3][y/3]-=s<<t;
}
inline int get(int x,int y)
{
return row[x]&col[y]&cell[x/3][y/3];
}
void dfs(int cnt,int score)
{
if(!cnt)
{
ans=max(max(ans,score));
return;
}
int x,y,mins=10;
for(int i=0;i<N;i++)
for(int j=0;j<N;j++)
if(!g[i][j])
{
int state=get(i,j);
if(ones[state]<mins)
{
mins=ones[state];
x=i;
y=j;
}
}
for(int i=get(x,y);i;i-=lowbit(i))
{
int t=log[lowbit(i)]+1;
draw(x,y,t);
dfs(cnt-1,score+t*get_score(x,y));
draw(x,y,-t);
}
}
int main()
{
init();
int cnt=0,score=0;
for(int i=0;i<N;i++)
{
int x;
scanf("%d",&x);
if(x)
{
draw(i,j,x);
score+=x*get_score(i,j);
}
else cnt++;
}
dfs(cnt,score);
printf("%d\n",ans);
return 0;
}