再读《挑战程序设计竞赛》——初出茅庐(1)

入门

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;
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值