【算法】算法笔记

算法专题

目录

一、树和图

1. 二叉树构造和遍历

给出二叉树的后序(postorder)遍历和中序(inorder)遍历,建立二叉树,并得到树的层次遍历(level traversal)

  • preorder:根 - 左子树 - 右子树
  • inorder:左子树 - 根 - 右子树 (即为深度优先)
  • postorder:左子树 - 右子树 - 根
  • level traversal:层次遍历,即广度优先
#include<bits/stdc++.h> 
using namespace std;

//由树的中序和后序=>层析遍历 
typedef struct BiTree{
	int num;
	BiTree *left;
	BiTree *right;
}BT;
int post[10010];
int in[10010];
BT *T;
//建立二叉树
BT* CreateBiTree(int postl,int postr,int inl,int inr){
	//创建二叉树
	
	//递归边界,重要! 
	if(postl > postr){
		return NULL;
	}
	//printf("--------------------\n");
	//寻找根节点 
	int rooti;
	for(int i = inl;i<=inr;i++){
		if(in[i] == post[postr]){
			rooti = i;
			break;
		}
	}
	BT *node = (BT*)malloc(sizeof(BiTree));
	node->num = in[rooti];
	
	node->left = CreateBiTree(postl,postl+(rooti-inl)-1,inl,rooti-1);
	node->right = CreateBiTree(postl+(rooti-inl),postr-1,rooti+1,inr); 
	
	return node;
}

//层析遍历,借助queue
void bfs(BT* root) {
	queue<BT*> q;
	q.push(root);
	while(!q.empty()){
		BT *temp = q.front();
		q.pop();
		printf("%d ",temp->num);
		if(temp->left) q.push(temp->left);
		if(temp->right) q.push(temp->right);
	}
}

int main(){
	int n;
	cin>>n;
	//输入后序 
	for(int i = 0;i<n;i++){
		cin>>post[i];
	}
	//输入中序 
	for(int i = 0;i<n;i++){
		cin>>in[i];
	}

	T = CreateBiTree(0,n-1,0,n-1);
	bfs(T);
	
	return 0;
}

2. 朋友圈 - 并查集

#include<bits/stdc++.h> 
using namespace std;
int parent[10010];
void init(int n){
	for(int i = 0;i<=n;i++)
		parent[i] = -1;
}
//寻找祖先+路径压缩 
int find(int x){
	int s;
	for(s = x;parent[s] > 0; s = parent[s]);
	while(s!=x){
		int tmp = parent[x];
		parent[x] = s;
		x = tmp;
	}
	return s;
}
void Union(int a,int b){
	int fa = find(a),fb = find(b);
	int tmp = parent[fa] + parent[fb];
	if(parent[fa] > parent[fb]){
		parent[fa] = fb;
		parent[fb] = tmp;
	}else{
		parent[fa] = tmp;
		parent[fb] = fa;
	}
}

int main(){
	int n,m;
	cin>>n>>m; 
	int na,a[10010];
	init(n);
	for(int i = 0;i<m;i++){
		cin>>na;
		for(int j = 0;j<na;j++){
			cin>>a[j];
		}
		for(int j = 1;j<na;j++){
			if(find(a[j])!=find(a[j-1])){
				Union(a[j],a[j-1]);
			}
		}
	}
	int mins = 100100;
	for(int i =1;i<=n;i++){
		if(parent[i]<0){
			if(parent[i] < mins){
				mins = parent[i];
			}
		}
	}
	cout<<-mins<<endl;
}

3. 公共朋友 - 非朋友圈

poj4109
描述:小明和小红去参加party。会场中总共有n个人,这些人中有的是朋友关系,有的则相互不认识。朋友关系是相互的,即如果A是B的朋友,那么B也是A的朋友。小明和小红想知道其中某两个人有多少个公共的朋友。
输入:第一行为一个正整数c,代表测试数据的个数。接下来是c组测试数据。 对于每组测试数据,第一行是三个数字n(2<=n<=100),m和k,分别表示会场中的人数,已知的朋友关系数目,问题的数目。接下来的m行,每行用两个数字i和j(1<=i,j<=n)表示了一个朋友关系,表示第i个人和第j个人是朋友关系。接下来的k行,每行用两个数字i和j(1<=i,j<=n)表示一个问题,请问第i个人和第j个人有多少公共的朋友。
输出: 对于第i组测试数据,首先输出一行”Case i:”,接下来得k行代表了k个问题,每行输出第i个人和第j个人有多少公共的朋友。

利用set去重:

#include<bits/stdc++.h>
using namespace std;
vector<int> g[104];

int main(){
	int Case;
	cin>>Case;
	for(int cases = 1;cases<=Case;cases++){
		
		for(int i = 0;i<100;i++){
			g[i].clear();
		}
		
		int n,m,k;
		cin>>n>>m>>k;
		int a,b;
		for(int i = 0;i<m;i++){
			cin>>a>>b;
			g[a].push_back(b);
			g[b].push_back(a);
		}
		cout<<"Case "<<cases<<":"<<endl;
		for(int i = 0;i<k;i++){
			cin>>a>>b;
			set<int> s;
			int n1 = g[a].size();
			int n2 = g[b].size();
			for(int i = 0;i<n1;i++){
				s.insert(g[a][i]);
			}
			for(int i = 0;i<n2;i++){
				s.insert(g[b][i]);
			}
			int n3 = s.size();
			cout<<n1+n2-n3<<endl;
		}
	}

	return 0;
}

4. 哈夫曼树

哈夫曼树的定义

定义:哈夫曼树即为最小二叉树。所谓最优指的是树的带权路径长度最小,即:二叉树的树枝赋权值,从根节点到所有叶子节点的路径上权值之和最小,这样的树为哈夫曼树。相应的应用是哈夫曼编码

构造哈夫曼树 - 优先队列实现

题目: CSU - 1588
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。  每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。  因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。  例如有3种果子,数目依次为1,2,9。可以先将1、2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。
#include<bits/stdc++.h>
using namespace std;

typedef long long ll;
//优先队列实现哈夫曼树
int n;
priority_queue<ll,vector<ll>,greater<ll> >  que;
int main(){
	cin>>n;
	int x;
	for(int i = 0;i<n;i++){
		cin>>x;
		que.push(x);
	}
	int ans = 0; 
	while(que.size()>1){
		ll a1 = que.top();
		que.pop();
		ll a2 = que.top();
		que.pop();
		ans+=a1+a2;
		que.push(a1+a2);
	}
	cout<<ans<<endl;
	return 0;
}

5. 其他二叉树性质相关计算

  • poj 2788 (√)

  • poj 2756 (√)

6. 图的连通分量

寻找一个图中满足条件的所有连通分量

九度1446:[Head of a Gang]( <http://ac.jobdu.com/problem.php?pid=1446 )
描述:One way that the police finds the head of a gang is to check people's phone calls. If there is a phone call between A and B, we say that A and B is related. The weight of a relation is defined to be the total time length of all the phone calls made between the two persons. A "Gang" is a cluster of more than 2 persons who are related to each other with total relation weight being greater than a given threthold K. In each gang, the one with maximum total weight is the head. Now given a list of phone calls, you are supposed to find the gangs and the heads.
输入:第一行两个整数,电话数n和阈值k。接下来n行,(A B c),每行一个电话,表示A打给B,且通话时间为c。注意,名字A和B是由三个大写字母组成的。
输出:第一行表示团伙数n,接下来n行每行为一个团伙,输出为该团伙的头目名字和团伙成员数。

样例:

输入:

8 59
AAA BBB 10
BBB AAA 20
AAA CCC 40
DDD EEE 5
EEE DDD 70
FFF GGG 30
GGG HHH 20
HHH FFF 10

输出:

2
AAA 3
GGG 3

题目分析

  • 找图中所有的连通子图。注意题目描述不清楚,实际上,只要连通就算一个团伙,而不是一定要子图为完全图。
  • 用并查集,可以找到所有连通块。
  • 判断连通块的通话时间总数是否满足阈值,需要找到该连通块所有的节点编号,故同时使用邻接矩阵存储,用dfs找到所有节点并保存。
  • 找头目。找连接图中权值最大的点,故同时用数组a[MAX]记录每个点的权值之和,在输入时就记录。
  • 用优先队列(pair)存储所有找到的头目结点编号以及该团伙人数,最后按序输出即可。
  • 注意输入的名字是三个大写字母,故将其hash成整数,最后输出再转回字符数组。

解决

#include<bits/stdc++.h>
using namespace std;

int parent[18005];
int node[18005];
int n,k;
vector<int > g[18000];
vector<int> que;
int vis[18000];
typedef pair<int,int > P;
priority_queue<P,vector<P>,greater<P> > res;
void init(){
	for(int i = 0;i<18000;i++){
		parent[i]=-1;
		node[i] = 0;
	} 
}

int hashName(const char name[]){
	return (name[0]-'A')*26*26 + (name[1] - 'A') *26 +(name[2] -'A');
}
void intToName(int x,char name[4]){
	char a = x%26+'A';
	char b = x%(26*26)/26 +'A';
	char c = x/26/26 + 'A';
	name[0] = a;name[1] = b;name[2] = c;name[3]='\0';
}

int find(int x){
	int s;
	for(s = x;parent[s]>0;s=parent[s]);
	while(s!=x){
		int tmp = parent[x];
		parent[x] = s;
		x = tmp;
	}
	return s;
}
void Union(int a,int b){
	int fa = find(a);
	int fb = find(b);
	int tmp = parent[fa] + parent[fb];
	if(parent[fa] > parent[fb]){
		parent[fb] = tmp;
		parent[fa] = fb;
	}else{
		parent[fb] = fa;
		parent[fa] = tmp;
	}
}

void dfs(int x){
	que.push_back(x);
	vis[x] = 1;
	for(int i = 0;i<g[x].size();i++){
		if(!vis[g[x][i]]){//未访问 
			dfs(g[x][i]);
		}
	}
}

int main(){
	cin>>n>>k;
	char name1[4],name2[4];
	int cost;
	init();
	for(int i = 0;i<n;i++){
		//scanf("%s%s%d",name1,name2,cost);
		cin>>name1>>name2>>cost;
		int na1 = hashName(name1);
		int na2 = hashName(name2);
		if(find(na1)!=find(na2)){
			Union(na1,na2);
		}
		node[na1]+=cost;
		node[na2]+=cost;
		g[na1].push_back(na2);
		g[na2].push_back(na1);
	}
	for(int i = 0;i<=17575;i++){
		if(parent[i] <=-3){//满足一个群 
			//判断总通话是否大于阈值 
			////dfs寻找这个群中所有的节点标号
			que.clear();
			memset(vis,0,sizeof(vis));
			dfs(i);
			////所有节点保存在que中了
			////计算总权值
			int totalV = 0;
			int maxx = -1;
			int maxi = 0;
			for(int i = 0;i<que.size();i++){
				totalV+= node[que[i]];
				if(node[que[i]]>maxx){
					maxx = node[que[i]]; 
					maxi = que[i];
				}
			}
			totalV/=2;
			if(totalV > k ){ //满足
				//最大点是maxi 
				char namemax[4];
				res.push(P(maxi,que.size()));
			}
		}
	}
	
	cout<<res.size()<<endl;
	char nameres[4];
	while(!res.empty()){
		intToName(res.top().first,nameres);
		cout<<nameres<<" "<<res.top().second<<endl;
		res.pop();
	}
	
	return 0;
}

7. 最小生成树

灾后重建

Pear市一共有N(<=50000)个居民点,居民点之间有M(<=200000)条双向道路相连。这些居民点两两之间都可以通过双向道路到达。这种情况一直持续到最近,一次严重的地震毁坏了全部M条道路。
震后,Pear打算修复其中一些道路,修理第i条道路需要Pi的时间。不过,Pear并不打算让全部的点连通,而是选择一些标号特殊的点让他们连通。
Pear有Q(<=50000)次询问,每次询问,他会选择所有编号在[l,r]之间,并且 编号 mod K  = C 的点,修理一些路使得它们连通。由于所有道路的修理可以同时开工,所以完成修理的时间取决于花费时间最长的一条路,即涉及到的道路中Pi的最大值。

你能帮助Pear计算出每次询问时需要花费的最少时间么?这里询问是独立的,也就是上一个询问里的修理计划并没有付诸行动。

【输入格式】
第一行三个正整数N、M、Q,含义如题面所述。
接下来M行,每行三个正整数Xi、Yi、Pi,表示一条连接Xi和Yi的双向道路,修复需要Pi的时间。可能有自环,可能有重边。1<=Pi<=1000000。

接下来Q行,每行四个正整数Li、Ri、Ki、Ci,表示这次询问的点是[Li,Ri]区间中所有编号Mod Ki=Ci的点。保证参与询问的点至少有两个。

【输出格式】
输出Q行,每行一个正整数表示对应询问的答案。

【样例输入】
7 10 4
1 3 10
2 6 9
4 1 5
3 7 4
3 6 9
1 5 8
2 7 4
3 2 10
1 7 6
7 6 9
1 7 1 0
1 7 3 1
2 5 1 0
3 7 2 1

【样例输出】
9
6
8
8

【数据范围】
对于20%的数据,N,M,Q<=30
对于40%的数据,N,M,Q<=2000
对于100%的数据,N<=50000,M<=2*10^5,Q<=50000. Pi<=10^6. Li,Ri,Ki均在[1,N]范围内,Ci在[0,对应询问的Ki)范围内。




资源约定:
峰值内存消耗 < 256M
CPU消耗  < 5000ms

解决

#include<bits/stdc++.h>
using namespace std;

struct edge{
	int from,to;
	int cost;
	edge(){}
	edge(int f,int t,int c):from(f),to(t),cost(c){}
};
int n,m,q;
edge edges[200005];
int parent[50010];
int node[50006]; //要联通的点临时保存 
int node_num;

//重写cmp, qsort使用 
int cmp(const void *a, const void *b){
	edge e1 = *(edge*)a;
	edge e2 = *(edge*)b;
	if(e1.cost > e2.cost) return 1;
	else if(e1.cost < e2.cost) return -1;
	else{
		return 0;
	}
}
//并查集init
void init(int n){
	for(int i = 0;i<=n;i++){
		parent[i] = -1;
	}
}
//寻找祖先,路径压缩 
int find(int x){
	int s;
	for(s = x;parent[s]>=0;s=parent[s]);
	while(s!=x){
		int tmp = parent[x];
		parent[x] = s;
		x = tmp;
	}
	return s;
}
//合并 
void Union(int a,int b){
	int fa = find(a);
	int fb = find(b);
	int tmp = parent[fa] + parent[fb];
	if(parent[fa] > parent[fb]){
		parent[fa] = fb;
		parent[fb] = tmp;
	}else{
		parent[fb] = fa;
		parent[fa] = tmp;
	}
}
//判断是否结束 
int judge(int a[50010],int cnt){
	int parents = find(a[0]);
	for(int i = 1;i<cnt;i++){
		if(find(a[i]) != parents){
			return 0;
		}
	}
	return 1;
}
//克鲁斯卡尔 
int Kruskul(){
	init(n);
	qsort(edges,m,sizeof(edges[0]),cmp);
	//n个点,共n-1条边就可以 
	int mincost = -1;
	for(int i = 0;i<n-1;i++){
		edge et = edges[i];
		int from = et.from;
		int to = et.to;
		if(find(from) != find(to)){
			Union(from,to); //合并
			if(et.cost > mincost){
				mincost = et.cost;
			}
			//判断是否结束
			if(judge(node,node_num)){
				break;
			}
		}
	}
	return mincost;
}

int main(){
	cin>>n>>m>>q;
	for(int i = 0;i<m;i++){
		int x,y,p;
		cin>>x>>y>>p;
		edge e = edge(x,y,p);
		edges[i] = e;
	}
	for(int i = 0;i<q;i++) {
		int l,r,k,c;
		cin>>l>>r>>k>>c;
		node_num = 0;
		for(int j = l;j<=r;j++)	{
			if(j % k ==  c){
				node[node_num++] = j;
			}
		}
		int ans = Kruskul();
		cout<<ans<<endl;
	}
	return 0;
}

8. 单源最短路径 - dijkstra

A traveler's map gives the distances between cities along the highways, together with the cost of each highway. Now you are supposed to write a program to help a traveler to decide the shortest path between his/her starting city and the destination. If such a shortest path is not unique, you are supposed to output the one with the minimum cost, which is guaranteed to be unique.

Input Specification:

Each input file contains one test case. Each case starts with a line containing 4 positive integers N, M, S, and D, where N (≤500) is the number of cities (and hence the cities are numbered from 0 to N−1); M is the number of highways; S and D are the starting and the destination cities, respectively. Then M lines follow, each provides the information of a highway, in the format:

City1 City2 Distance Cost
where the numbers are all integers no more than 500, and are separated by a space.

Output Specification:

For each test case, print in one line the cities along the shortest path from the starting point to the destination, followed by the total distance and the total cost of the path. The numbers must be separated by a space and there must be no extra space at the end of output.

Sample Input:

4 5 0 3
0 1 1 20
1 3 2 30
0 3 4 10
0 2 2 20
2 3 1 20

Sample Output:

0 2 3 3 40

分析

  • 单源最短路径问题。
  • 注意这里两个度量,即距离最短,相同最短距离的路径取代价最小。
  • 记录最短路径。

解决

#include<bits/stdc++.h>
#define MAX 10010
#define INF 1001
using namespace std;

struct edge{
	
	int from;
	int to;
	int dis;
	int cost;
	edge(){}
	edge(int t,int d,int c):to(t),dis(d),cost(c){}
	
};
vector<edge> g[MAX];
typedef pair<int,int> P;
int D[MAX];
int C[MAX];
int n,m;
int pre[MAX];
void Dijkstra(int begin,int end){
	fill(D,D+MAX,INF);
	for(int i = 0;i<n;i++){
		pre[i] = -1;
		C[i] = 0;
	}
	D[begin] = 0;
	priority_queue<P,vector<P>,greater<P> > que;
	que.push(P(D[begin],begin));
	while(!que.empty()){
		P p = que.top();
		que.pop();
		int v = p.second;
		if(D[v] < p.first) continue;
		for(int i = 0;i<g[v].size();i++){
			edge e = g[v][i];
			int to = e.to;
			int dis = e.dis;
			if(D[v] + dis < D[e.to]){
				pre[to] = v;
				D[e.to] = D[v] + dis;
				que.push(P(D[to],to));
				C[to] = (C[v] + e.cost);
			}else if(D[v]+dis == D[e.to]){
				if(C[v]+e.cost < C[to]){
					pre[to] = v;
					D[e.to] = D[v] + dis;
					que.push(P(D[to],to));
					C[to] = (C[v] + e.cost);				
				}
			}
		}
	}
}

int main(){
	int a,b,c,d;	
	int begin,end;
	
	cin>>n>>m;
	cin>>begin>>end;
	
	for(int i = 0;i<m;i++){
		cin>>a>>b>>c>>d;
		g[a].push_back(edge(b,c,d));
		g[b].push_back(edge(a,c,d));
	}

	Dijkstra(begin,end);
	stack<int> st;
	st.push(end);
	int now = end;
	while(pre[now]>= 0){
		now = pre[now];
		st.push(now);
	}
	while(!st.empty()){
		cout<<st.top()<<" "; 
		st.pop();
	}
	cout<<D[end]<<" "<<C[end]<<endl;
	
	return 0;
}

二、枚举搜索

1. 按钮开关问题

若局部被确定则整体状态被确定

枚举第一个按钮的状态(这里第一个按钮作为局部,一旦确定就可推出全部按钮状态)

问题1:特殊密码锁

## 特殊密码锁
*描述*
有一种特殊的二进制密码锁,由n个相连的按钮组成(n<30),按钮有凹/凸两种状态,用手按按钮会改变其状态。
然而让人头疼的是,当你按一个按钮时,跟它相邻的两个按钮状态也会反转。当然,如果你按的是最左或者最右边的按钮,该按钮只会影响到跟它相邻的一个按钮。
当前密码锁状态已知,需要解决的问题是,你至少需要按多少次按钮,才能将密码锁转变为所期望的目标状态。

*输入*
两行,给出两个由0、1组成的等长字符串,表示当前/目标密码锁状态,其中0代表凹,1代表凸。

*输出*
至少需要进行的按按钮操作次数,如果无法实现转变,则输出impossible。

*样例输入*
011
000
*样例输出*
1

分析

  • 枚举法:每个按钮有2种状态,但是最多可能有30个灯,因此状态有2^30之多,穷举一定会超时。

  • 点1:一个灯如果按了第二下,就会抵消上一次按下所产生的影响。因此,一个灯只有按或者不按两种情况,不存在一个灯要开关多次的情况。

    例如八个灯 00000000

    按1后 11000000
    按3后 10110000
    按1后 01110000
    这和八个灯 00000000
    只按一次3后 01110000
    是完全相同的情况

  • 点2 :我们只需要考虑是否按下第一个灯。因为如果第一个灯的状态被确定了,那么是否按下第二个灯也就决定了(如果第一个灯与期望不同,则按下,如果期望相同,则不按下)同理,第三个灯是否按下也唯一确定。所以,本题只要分两种情况:灯1被按下和没有被按下。之后使用for循环判断别的灯是否需要按下即可,当循环结束,若现在的灯况与答案相同(只需要判定最后一个灯是否相同),则输出两种方案中按键次数最少的,若不同,则impossible!

解决

#include<bits/stdc++.h> 
using namespace std;

char s[100],re[100],s_temp[100];
int minp = 1001;
int n; 
void push1(int i){
	for(int j = max(0,i-1);j<min(n,i+2);j++){
		if(s[j] == '0') 
			s[j] = '1';
		else
			s[j] = '0';
	}
}
void push2(int i){
	for(int j = max(0,i-1);j<min(n,i+2);j++){
		if(s_temp[j] == '0') 
			s_temp[j] = '1';
		else
			s_temp[j] = '0';
	}
}

int main(){
	scanf("%s",s);
	scanf("%s",re);
	int len = strlen(s);
	n = len;
	strcpy(s_temp,s);
	//printf("%s %s\n",re,reT);
	//第一个按钮按下 
	int cnt = 0;
	push1(0);
	cnt++;
	for(int i = 1;i<len;i++){ //依次判断其余灯是否需要被按下 
		if(s[i-1] != re[i-1]){ //上一个相同,而上一个也按下了而导致不同,则这个也得按下
			push1(i);
			cnt++;
		}
	}
	//判断是否成功了
	int flag = 1;
	if(s[len-1] == re[len-1]){
		minp = min(cnt,minp);	
	}

	//第一个按钮不按下 
	cnt = 0;
	for(int i = 1;i<len;i++){ //依次判断其余灯是否需要被按下 
		if(s_temp[i-1] != re[i-1]){ //上一个不同,则这个也得按下
			push2(i);
			cnt++;
		}
	}
	//判断是否成功了
	if(s_temp[len-1] == re[len-1]){
		minp = min(minp,cnt);
	}

	if(minp >=1001){
		cout<<"impossible"<<endl;
	}else{
		cout<<minp<<endl;
	}

	return 0;
}
/*
 *  ┏┓   ┏┓ 
 *┏┛┻━━━┛┻┓ 
 *┃       ┃   
 *┃   ━   ┃ 
 *┃ ┳┛ ┗┳ ┃ 
 *┃       ┃ 
 *┃   ┻   ┃ 
 *┃       ┃ 
 *┗━┓   ┏━┛ 
 *  ┃   ┃神兽
 *  ┃   ┃镇bug
 *  ┃   ┗━━━┓ 
 *  ┃       ┣┓ 
 *  ┃       ┏┛ 
 *  ┗┓┓┏━┳┓┏┛ 
 *   ┃┫┫ ┃┫┫ 
 *   ┗┻┛ ┗┻┛  
 *    
 */

问题2:熄灯问题

即二维的特殊密码锁,这里一个局部是第一行的操作状态,通过枚举第一行的操作状态得到所有灯的操作。

*描述
有一个由按钮组成的矩阵,其中每行有6个按钮,共5行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上边、下边、左边、右边)的灯都会改变一次。即,如果灯原来是点亮的,就会被熄灭;如果灯原来是熄灭的,则会被点亮。在矩阵角上的按钮改变3盏灯的状态;在矩阵边上的按钮改变4盏灯的状态;其他的按钮改变5盏灯的状态。
需要按下哪些按钮,恰好使得所有的灯都熄灭。

*Input
5行组成,每一行包括6个数字(0或1)。相邻两个数字之间用单个空格隔开。0表示灯的初始状态是熄灭的,1表示灯的初始状态是点亮的。

*Output
5行组成,每一行包括6个数字(0或1)。相邻两个数字之间用单个空格隔开。其中的1表示需要把对应的按钮按下,0则表示不需要按对应的按钮。

样例输入

0 1 1 0 1 0
1 0 0 1 1 1
0 0 1 0 0 1
1 0 0 1 0 1
0 1 1 1 0 0

样例输出

1 0 1 0 0 1
1 1 0 1 0 1
0 0 1 0 1 1
1 0 0 1 0 0
0 1 0 0 0 0

分析

  • 二维的特殊锁问题
  • 枚举第一行(或第一列)所有状态,以此确定以后各行状态,最后判断是否符合题意

解决

//此段代码没有通过,仅仅通过了样例,尚未找出bug
#include<bits/stdc++.h>
using namespace std;


int a[5][6];
int a_temp[5][6];
int re[5][6];
int result[5][6];
void push(int x,int y){
	for(int i = max(0,y-1);i<=min(5,y+1);i++){
		a[x][i] = !a[x][i];
	}
	for(int i = max(0,x-1);i<=min(4,x+1);i++){
		a[i][y] = !a[i][y];
	}
	a[x][y] = !a[x][y];//注意上面(x,y)两次取反则没变化 
}

int main(){
	for(int i = 0;i<5;i++){
		for(int j = 0;j<6;j++){
			cin>>a[i][j];
			re[i][j] = 0;
		}
	}
	//memcpy(a_temp,a,sizeof(a));
	for(int l1= 0;l1<5;l1++){
		for(int l2 = 0;l2<6;l2++){
			a_temp[l1][l2] = a[l1][l2];
		}
	}
	push(2,2);
	
	int sucess = 0;
	for(int i = 0;i<63;i++){
		//按下第一行
		//每次都初始化将a变回开始值 
		//memcpy(a,a_temp,sizeof(a_temp));
		for(int l1= 0;l1<5;l1++){
			for(int l2 = 0;l2<6;l2++){
				a[l1][l2] = a_temp[l1][l2];
			}
		}
		//初始化结果result 
		for(int j = 0;j<5;j++){
			for(int k = 0;k<6;k++){
				result[j][k] = 0;
			}
		}
		//第一行按灯 
		if(i&32){
			push(0,0);
			result[0][0] = 1;
		}			
		if(i&16){
			push(0,1);
			result[0][1] = 1;	
		}		
		if(i&8){
			push(0,2);		
			result[0][2] = 1;
		}
		if(i&4){
			push(0,3);	
			result[0][3] = 1;
		}
		if(i&2){
			push(0,4);		
			result[0][4] = 1;
		}
		if(i&1){
			push(0,5);	
			result[0][5] = 1;
		}			

		//每行 
		for(int j = 1;j<5;j++){ //每行			
			for(int k = 0; k < 6; k++){
				if(a[j-1][k]!=0){
					push(j,k);
					result[j][k] = 1;
				}
			}
		}
		
		//判断是否成功
		int flags = 1;
		for(int l2 = 0;l2<6;l2++)	{
			if(a[4][l2]!=0) {
				flags = 0;
				break;
			}
		}
		if(flags){
			sucess = 1;
			break;
		}	
	}
	if(sucess){
		for(int i = 0;i<5;i++){
			for(int j = 0;j<5;j++){
				cout<<result[i][j]<<" ";
			}
			cout<<result[i][5]<<endl;
		}
	}
	return 0;
}
//此段代码AC,@北大郭炜老师
#include<memory>
#include<string>
#include<cstring>
#include<iostream>
using namespace std;
int GetBit(char c,int i)//取c的第i位
{  return ( c >> i ) & 1;  }
void SetBit(char & c,int i, int v)//设置c的第i位设为v 
{
    if( v )  c |= ( 1 << i);
    else   c &= ~( 1 << i);
}
void Flip(char & c, int i)//将c的第i位取反 
{  c ^= ( 1 << i);  }
void OutputResult(int t,char result[]) //输出结果
{
    //cout << "PUZZLE #" << t << endl;
    for( int i = 0;i < 5; ++i )
    {
        for( int j = 0; j < 6; ++j )
        {
            cout << GetBit(result[i],j);
            if( j < 5 ) cout << " ";
        }
        cout << endl;
    }
}
int main()
{
    char oriLights[5]; //最初灯矩阵,一个比特表示一盏灯
    char lights[5]; //不停变化的灯矩阵
    char result[5]; //结果开关矩阵
    char switchs; //某一行的开关状态
    int T;
    //cin >> T;//POJ原题需要输入T表示有T组测试数据
    T=1;
    for( int t = 1; t <= T; ++ t)
    {
        memset(oriLights,0,sizeof(oriLights));
        for( int i = 0;i < 5; i ++ )//读入最初灯状态
        { 
            for( int j = 0; j < 6; j ++ )
            {
                int s;
                cin >> s;
                SetBit(oriLights[i],j,s);
            }
        }
        
        for( int n = 0; n < 64; ++n )//遍历首行开关的64种操作
        { 
            memcpy(lights,oriLights,sizeof(oriLights));
            switchs = n; //先假定第0行的开关需要的操作方案
            for( int i = 0;i < 5; ++i )
            {
                result[i] = switchs; //保存第i行开关的操作方案
                
                for( int j = 0; j < 6; ++j )//根据方案修改第i行的灯
                {
                    if( GetBit(switchs,j))
                    {   //switchs的第j个位等于1表示需要按下第i行第j个按钮,等于0表示不需要按下该按钮
                        if( j > 0) Flip(lights[i],j-1);//改左灯
                        Flip(lights[i],j);//改开关位置的灯
                        if( j < 5 ) Flip(lights[i],j+1);//改右灯
                    }
                }
                if( i < 4 ) lights[i+1] ^= switchs;//改下一行的灯
                
                switchs = lights[i]; //第i+1行开关的操作方案由第i行灯的状态决定
            }
            if( lights[4] == 0 )
            {
                OutputResult(t,result);
                break;
            }
        } // for( int n = 0; n < 64; n ++ )
    }
    return 0;
}

问题3:画家问题

*描述

有一个正方形的墙,由<N*N>个正方形的砖组成,其中一些砖是白色的,另外一些砖是黄色的。Bob是个画家,想把全部的砖都涂成黄色。但他的画笔不好使。当他用画笔涂画第(i, j)个位置的砖时, 位置(i-1, j)、 (i+1, j)、 (i, j-1)、 (i, j+1)上的砖都会改变颜色。请你帮助Bob计算出最少需要涂画多少块砖,才能使所有砖的颜色都变成黄色。

*输入
第一行是一个整数n (1≤n ≤15),表示墙的大小。接下来的n行表示墙的初始状态。每一行包含n个字符。第i行的第j个字符表示位于位置(i,j)上的砖的颜色。“w”表示白砖,“y”表示黄砖。

*输出
一行,如果Bob能够将所有的砖都涂成黄色,则输出最少需要涂画的砖数,否则输出“inf”。

*样例输入
5
wwwww
wwwww
wwwww
wwwww
wwwww

*样例输出
15 

分析

  • 类似于二维按钮开关,即上题熄灯问题

解决

//AC
//注意位运算枚举的技巧

#include<bits/stdc++.h> 
using namespace std;

char a[20][20];
char a_tmp[20][20];
char result[20][20];
int n;
void push(int x,int y){
	for(int i = max(x-1,0);i<=min(n-1,x+1);i++){
		if(a[i][y] == 'w')
			a[i][y] = 'y';
		else
			a[i][y] = 'w';
	}
	for(int i = max(0,y-1);i<=min(n-1,y+1);i++){
		if(i == y)
			continue;
		if(a[x][i] == 'w')
			a[x][i] = 'y';
		else
			a[x][i] = 'w';
	}
}

int main(){
	cin>>n;
	for(int i = 0;i<n;i++){
		for(int j = 0;j<n;j++){
			result[i][j] = 'y';
		}
	}
	for(int i = 0;i<n;i++){
		scanf("%s",a[i]);
	}
	for(int i = 0;i<n;i++){
		for(int j = 0;j<n;j++){
			a_tmp[i][j] = a[i][j];
		}
	}
	int cnt = 0;
	int minp = 1000;
	for(int t = 0;t<pow(2,n);t++){
		cnt = 0;
		for(int i = 0;i<n;i++){
			for(int j = 0;j<n;j++){
				a[i][j] = a_tmp[i][j];
			}
		}
		//第一行处理 
		for(int i = 0;i<n;i++){
			if(t&(1<<i)){
				push(0,i);
				cnt++;
			}
		}
		//以后各行处理
		for(int i = 1;i<n;i++) {
			for(int j = 0;j<n;j++){
				if(a[i-1][j]!=result[i-1][j]){
					push(i,j);
					cnt++;
				}
			}
		}
		//判断是否成功
		int flag = 1;
		for(int i = 0;i<n;i++){
			if(a[n-1][i] != 'y'){
				flag = 0;
				break;
			}
		}
		if(flag){
			if(cnt < minp)
				minp = cnt;			
		}
	}
	
	if(minp < 1000){
		cout<<minp<<endl;
	}else{
		cout<<"inf"<<endl;
	}

	return 0;
}

2. 多层枚举问题

做一下这个:2694:逆波兰表达式

问题1: 生理周期

这个题有个坑。

*描述

人生来就有三个生理周期,分别为体力、感情和智力周期,它们的周期长度为23天、28天和33天。每一个周期中有一天是高峰。在高峰这天,人会在相应的方面表现出色。例如,智力周期的高峰,人会思维敏捷,精力容易高度集中。因为三个周期的周长不同,所以通常三个周期的高峰不会落在同一天。对于每个人,我们想知道何时三个高峰落在同一天。对于每个周期,我们会给出从当前年份的第一天开始,到出现高峰的天数(不一定是第一次高峰出现的时间)。你的任务是给定一个从当年第一天开始数的天数,输出从给定时间开始(不包括给定时间)下一次三个高峰落在同一天的时间(距给定时间的天数)。例如:给定时间为10,下次出现三个高峰同天的时间是12,则输出2(注意这里不是3)。

*输入
一行,包含四个整数:p, e, i和d,相邻两个整数之间用单个空格隔开。 p, e, i分别表示体力、情感和智力高峰出现的时间(时间从当年的第一天开始计算)。d 是给定的时间,可能小于p, e, 或 i。 所有给定时间是非负的并且小于等于365, 所求的时间小于等于21252。

*输出
一个整数,即从给定时间起,下一次三个高峰同天的时间(距离给定时间的天数)。

*样例输入
4 5 6 7

*样例输出
16994

分析

  • 坑1:给出的到出现高峰的天数不一定是第一次高峰出现的时间,比如p是第一次,而e是第二次,i是第三次都有可能。

  • 坑2:所求的时间小于等于21252,那么暴力搜索的时间应该是小于等于21252+d!

解决

版本1:枚举三个高峰期+剪枝

#include<bits/stdc++.h>
using namespace std;

int main(){
	int p,e,i,d;
	while(cin>>p>>e>>i>>d && p+e+i+d!=-4){
		int ans = 0;
		int px = p/23;
		int ex = e/28;
		int ix = i/33;
		for(int a1 = p-px*23;a1<=d+21252;a1+=23){
			for(int a2 = e-ex*28;a2<=d+21252;a2+=28){
				if(a1 != a2){
					continue;
				}
				for(int a3 = i-ix*33;a3<=d+21252;a3+=33){
					if(a2 != a3){
						continue;
					}
					if(a1 >= d){
						ans = a1 - d;
						break;
					}
				}
			}
		}		
		cout<<ans<<endl;		
	}

	return 0;
}

版本2:直接枚举所有时间,判断是否为高峰期

#include<bits/stdc++.h>
using namespace std;
int main(){
	int p,e,i,d;
	while(cin>>p>>e>>i>>d && p+e+i+d!=-4){
		int a;
		for(a = d+1;a<=d+21252;a++){
			if((a-p) %23 ==0 && (a-e)%28==0 && (a-i)%33==0){
				break;	
			}
		}
		cout<<a-d<<endl;
	}
	return 0;	
}

问题2: 拨钟问题

总时间限制: 1000ms 内存限制: 65536kB
    
*描述
有9个时钟,排成一个3*3的矩阵。

|-------|    |-------|    |-------|
|       |    |       |    |   |   |
|---O   |    |---O   |    |   O   |
|       |    |       |    |       |
|-------|    |-------|    |-------|
    A            B            C    

|-------|    |-------|    |-------|
|       |    |       |    |       |
|   O   |    |   O   |    |   O   |
|   |   |    |   |   |    |   |   |
|-------|    |-------|    |-------|
    D            E            F    

|-------|    |-------|    |-------|
|       |    |       |    |       |
|   O   |    |   O---|    |   O   |
|   |   |    |       |    |   |   |
|-------|    |-------|    |-------|
    G            H            I    
(图 1)
现在需要用最少的移动,将9个时钟的指针都拨到12点的位置。共允许有9种不同的移动。如下表所示,每个移动会将若干个时钟的指针沿顺时针方向拨动90度。
移动    影响的时钟
 1         ABDE
 2         ABC
 3         BCEF
 4         ADG
 5         BDEFH
 6         CFI
 7         DEGH
 8         GHI
 9         EFHI    

*输入
9个整数,表示各时钟指针的起始位置,相邻两个整数之间用单个空格隔开。其中,0=12点、1=3点、2=6点、3=9点。

*输出
输出一个最短的移动序列,使得9个时钟的指针都指向12点。按照移动的序号从小到大输出结果。相邻两个整数之间用单个空格隔开。

*样例输入
3 3 0 
2 2 2 
2 1 2 
    
*样例输出
4 5 8 9 

分析

  • 每个钟最多移动3次,故每个钟有4种移动方式-拨动0次、1次、2次、3次
  • 移动次序与结果无关,故总的枚举次数是4的9次方
  • 给出了9种移动方式。例如让1-9拨动i1到i9次,则就可以根据影响的钟,使得每个钟都处于4,则完成
  • 暴力,搜索移动次数最小的一种
  • 简单,但需要理解

解决

#include<bits/stdc++.h> 
using namespace std;

int a[10] = {0};
int res[10] = {0};
int main(){
	for(int i = 1;i<=9;i++){
		cin>>a[i];
	}
	
	//九层循环暴力
	int mincount = 10000;
	for(int i1 = 0;i1<4;i1++) {
		for(int i2 = 0;i2<4;i2++){
			for(int i3=0;i3<4;i3++){
				for(int i4=0;i4<4;i4++){
					for(int i5=0;i5<4;i5++){
						for(int i6=0;i6<4;i6++){
							for(int i7=0;i7<4;i7++){
								for(int i8=0;i8<4;i8++){
									for(int i9=0;i9<4;i9++){
					if(((i1+i2+i4+a[1])%4== 0)&&
					  ((i1+i2+i3+i5+a[2])%4==0) &&
					  ((i2+i3+i6+a[3])%4==0) &&
					  ((i1+i4+i5+i7+a[4])%4==0) &&
					  ((i1+i3+i7+i9+i5+a[5])%4==0) &&
					  ((i3+i5+i6+i9+a[6])%4==0) &&
					  ((i4+i7+i8+a[7])%4==0) &&
					  ((i5+i7+i8+i9+a[8])%4==0) &&
					  ((i8+i9+i6+a[9])%4==0)){
					  	int sums = i1+i2+i3+i4+i5+i6+i7+i8+i9;
					  	if(sums<mincount){
					  		mincount = sums;
					  		res[1] = i1;
							res[2] = i2;
					  		res[3] = i3;
							res[4] = i4;
					  		res[5] = i5;
							res[6] = i6;
					  		res[7] = i7;
							res[8] = i8;
					  		res[9] = i9;
						  }	  
					  } 
									}
								}
							}
						}
					}
				}
			}
		}
	}
	
	//打印结果
	for(int i = 1;i<=9;i++){
		while(res[i]--){
			printf("%d ",i);
			//res[i]--;
		}
	}
	cout<<endl;	
	return 0;
}

问题3:反正切函数的应用

@这题不错。

总时间限制: 1000ms 内存限制: 65536kB

*描述
反正切函数可展开成无穷级数,有如下公式
(其中0 <= x <= 1) 公式(1)
使用反正切函数计算PI是一种常用的方法。例如,最简单的计算PI的方法:
PI=4arctan(1)=4(1-1/3+1/5-1/7+1/9-1/11+...) 公式(2)
然而,这种方法的效率很低,但我们可以根据角度和的正切函数公式:
tan(a+b)=[tan(a)+tan(b)]/[1-tan(a)*tan(b)] 公式(3)
通过简单的变换得到:
arctan(p)+arctan(q)=arctan[(p+q)/(1-pq)] 公式(4)
利用这个公式,令p=1/2,q=1/3,则(p+q)/(1-pq)=1,有
arctan(1/2)+arctan(1/3)=arctan[(1/2+1/3)/(1-1/2*1/3)]=arctan(1)
使用1/2和1/3的反正切来计算arctan(1),速度就快多了。
我们将公式(4)写成如下形式
arctan(1/a)=arctan(1/b)+arctan(1/c)
其中a,b和c均为正整数。
我们的问题是:对于每一个给定的a(1 <= a <= 60000),求b+c的值。我们保证对于任意的a都存在整数解。如果有多个解,要求你给出b+c最小的解。

*输入
输入文件中只有一个正整数a,其中 1 <= a <= 60000。

*输出
输出文件中只有一个整数,为 b+c 的值。

*样例输入
1

*样例输出
5

分析

  • 通过化简,转换成b+c = 2a+t+(a2+1)/t,注意a2+1必须整除t。

  • 对于上个式子,枚举m,求最小值。

  • 1a=b+cbc−11a=b+cbc−1
    c=ab+1b−a(b>a)c=ab+1b−a(b>a)
    b+c=b+ab+1b−a=b+ab+1+a∗a−a∗ab−a=b+a+a∗a+1b−ab+c=b+ab+1b−a=b+ab+1+a∗a−a∗ab−a=b+a+a∗a+1b−a
    令t=b−at=b−a
    b+c=f(t)=t+2a+a∗a+1tb+c=f(t)=t+2a+a∗a+1t。

    以t=a为界限,左右枚举t。

  • 注意整形溢出!

解决

#include<iostream>
#include<algorithm>

using namespace std;

int main()
{
    unsigned int a, t, ans1, ans2;
    cin >> a;
    for (t = a; t > 0; t--)
    {
        if ((a*a + 1) % t == 0)
        {
            ans1 = t + 2 * a + (a*a + 1) / t;
            break;
        }
    }

    for (t = a + 1;; t++)
    {
        if ((a*a + 1) % t == 0)
        {
            ans2 = t + 2 * a + (a*a + 1) / t;
            break;
        }
    }

    cout << min(ans1, ans2) << endl;

    return 0;
}
//这个代码以t=a为界限,向左枚举,也通过;

//bc-1 = ab+ac
//因为b和c一定大于a
//故令b=a+m,c=a+n;
//则mn=a^2+1;
//b+c = 2a+m+(a^2+1)/m 枚举m算最小值
#include<stdio.h> 
typedef long long ll;
int main()
{
    ll a,m,ans;
    while(scanf("%lld",&a)!=EOF)
    {
        for(m=a;m>=1;m--)
            if((a*a+1)%m==0)break;
        ans=(a*a+1)/m+m+2*a;//b+c的值 
        printf("%lld\n",ans);
    }
    return 0;
}

三、递归搜索

如何输入一行字符串?可能包括空格。

方法1

#include<string>
string s;
getline(cin,s);
//注意,cin.getline(s,5);必须指定读入的字符个数,默认结束边界为'\n',添加第三个参数可以指定

方法2

#include<string>
char s[1001];
gets(s);

string 和 char 的相互转换*

string ->char * 需要调用函数:

const char *c = s.data();//或者 char *c = (char *)s.data();
const char *c = s.c_str();//或者 char *c = (char *)s.c_str();

char * -> string 直接赋值:

string s = c;

1. 简单递归

问题1:逆波兰表达式

总时间限制: 1000ms 内存限制: 65536kB

*描述
逆波兰表达式是一种把运算符前置的算术表达式,例如普通的表达式2 + 3的逆波兰表示法为+ 2 3。逆波兰表达式的优点是运算符之间不必有优先级关系,也不必用括号改变运算次序,例如(2 + 3) * 4的逆波兰表示法为* + 2 3 4。本题求解逆波兰表达式的值,其中运算符包括+ - * /四个。

*输入
输入为一行,其中运算符和运算数之间都用空格分隔,运算数是浮点数。

*输出
输出为一行,表达式的值。
可直接用printf("%f\n", v)输出表达式的值v。

*样例输入
* + 11.0 12.0 + 24.0 35.0

*样例输出
1357.000000

*提示
可使用atof(str)把字符串转换为一个double类型的浮点数。atof定义在math.h中。
此题可使用函数递归调用的方法求解。

解决

#include<bits/stdc++.h>
using namespace std;
string s;
double dfs(){
	string s;
	cin>>s;
	if(s.size()==1){
		if(s[0] == '+')
			return dfs()+dfs();
		else if(s[0] == '*')
			return dfs()*dfs();
		else if(s[0] == '-')
			return dfs()-dfs();
		else if(s[0] == '/')
			return dfs()/dfs();					
	}else{
		return atof(s.data());
	}
}
int main(){
	printf("%f\n",dfs());
}

2. 递增排列组合类

问题1:分解因数

排列组合类1:从起点开始递增的排列组合。可重复或者不可重复,且与顺序无关

总时间限制: 1000ms 内存限制: 65536kB

*描述
给出一个正整数a,要求分解成若干个正整数的乘积,即a = a1 * a2 * a3 * ... * an,并且1 < a1 <= a2 <= a3 <= ... <= an,问这样的分解的种数有多少。注意到a = a也是一种分解。

*输入
第1行是测试数据的组数n,后面跟着n行输入。每组测试数据占1行,包括一个正整数a (1 < a < 32768)

*输出
n行,每行输出对应一个输入。输出应是一个正整数,指明满足要求的分解的种数

*样例输入
2
2
20

*样例输出
1
4

分析

  • 这类题目,从起点开始,要求递增排列组合起来
  • 这类题目的核心:
    • 1 枚举所有起点(main中的for)
    • 2 枚举所有后继(dfs中的for)
    • 3 dfs(出口参数 , 条件参数1 , 条件参数2…) ; \(注意条件参数个数因题目限制的不同而有差异,具体可以对比下题注意条件参数个数因题目限制的不同而有差异,具体可以对比下题\)
    • 出口
    • 剪枝
    • 递增枚举
  • 若再要求因子不能重复,则思路大致相同,只不过枚举后继时,记得从前一个因子的下一个开始枚举

解决

#include<bits/stdc++.h>
using namespace std;
int n,a,ans;

void dfs(int k,int pre)//限定条件:递增 由参数pre实现(k用来判递归出口)
{
    if(k==a)
    {
        ans++;
        return ;
    }
    if(k>a) return ;
    for( int i=pre;i<=a;i++)//如果是不重复递增则i=pre+1
    {
        if(k*i<=a) //剪枝
            dfs(k*i,i);
        else break;
    }
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
    {
        ans=0;
        cin>>a;
        for(int i=2;i<=a;i++) dfs(i,i);//枚举所有起点,第一个因子是i,前面的乘积也是i
        cout<<ans<<endl;
    }
   return 0;
}

问题2:放苹果

排列组合类2:起点开始递增的排列组合且限制增加

总时间限制: 1000ms 内存限制: 65536kB

描述
把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用K表示)5,1,1和1,5,1 是同一种分法。

输入
第一行是测试数据的数目t(0 <= t <= 20)。以下每行均包含二个整数M和N,以空格分开。1<=M,N<=10。

输出
对输入的每组数据M和N,用一行输出相应的K。

样例输入
1
7 3
    
样例输出
8

分析

  • 相比上题目,一是改为加法,二是条件多了一个”项数不能超过m“。

解决

#include<bits/stdc++.h>
using namespace std;

int n,m;
int ans;
void dfs(int presum,int prem,int pre){
	if(prem > m){
		return;
	}
	if(presum == n){
		ans++;
		return;
	}
	if(presum > n){
		return;
	}
	
	for(int i = pre;i<=n;i++){
		if(i+presum <= n){
			dfs(i+presum,prem+1,i);
		}else{
			break;	
		}
	}
}
int main(){
	
	int t;
	cin>>t;
	while(t--){
		cin>>n>>m;
		ans = 0;
		for(int i = 1;i<=n;i++){
			dfs(i,1,i);
		}
		cout<<ans<<endl;
	}
	
	return 0;
}

问题3:碎纸机

排列组合类3:增加限制+综合上两题

*描述
你现在负责设计一种新式的碎纸机。一般的碎纸机会把纸切成小片,变得难以阅读。而你设计的新式的碎纸机有以下的
特点:
1.每次切割之前,先要给定碎纸机一个目标数,而且在每张被送入碎纸机的纸片上也需要包含一个数。
2.碎纸机切出的每个纸片上都包括一个数。
3.要求切出的每个纸片上的数的和要不大于目标数而且与目标数最接近。
举一个例子,如下图,假设目标数是50,输入纸片上的数是12346。碎纸机会把纸片切成4块,分别包含1,2,34和6。这样这些数的和是43 (= 1 + 2 + 34 + 6),这是所有的分割方式中,不超过50,而又最接近50的分割方式。又比如,分割成1,23,4和6是不正确的,因为这样的总和是34 (= 1 + 23 + 4 + 6),比刚才得到的结果43小。分割成12,34和6也是不正确的,因为这时的总和是52 (= 12 + 34 + 6),超过了50。
还有三个特别的规则:
1.如果目标数和输入纸片上的数相同,那么纸片不进行切割。
2.如果不论怎样切割,分割得到的纸片上数的和都大于目标数,那么打印机显示错误信息。
3.如果有多种不同的切割方式可以得到相同的最优结果。那么打印机显示拒绝服务信息。比如,如果目标数是15,输入纸片上的数是111,那么有两种不同的方式可以得到最优解,分别是切割成1和11或者切割成11和1,在这种情况下,打印机会显示拒绝服务信息。
为了设计这样的一个碎纸机,你需要先写一个简单的程序模拟这个打印机的工作。给定两个数,第一个是目标数,第二个是输入纸片上的数,你需要给出碎纸机对纸片的分割方式。

*输入
输入包括多组数据,每一组包括一行。每行上包括两个正整数,分别表示目标数和输入纸片上的数。已知输入保证:两个数都不会以0开头,而且两个数至多都只包含6个数字。
输入的最后一行包括两个0,这行表示输入的结束。

*输出
对每一组输入数据,输出相应的输出。有三种不同的输出结果:
sum part1 part2 ... 
rejected 
error 

第一种结果表示:
1.每一个partj是切割得到的纸片上的一个数。partj的顺序和输入纸片上原始数中数字出现的次序一致。
2.sum是切割得到的纸片上的数的和,也就是说:sum = part1 + part2 +...
第一种结果中相邻的两个数之间用一个空格隔开。

如果不论怎样切割,分割得到的纸片上数的和都大于目标数,那么打印“error”。
如果有多种不同的切割方式可以得到相同的最优结果,那么打印“rejected”。 

*样例输入
50 12346
376 144139
927438 927438
18 3312
9 3142
25 1299
111 33333
103 862150
6 1104
0 0

*样例输出
43 1 2 34 6
283 144 139
927438 927438
18 3 3 12
error
21 1 2 9 9
rejected
103 86 2 15 0
rejected

分析

  • 限制增多
  • 记录符合条件的”排列方式“

解决

#include<bits/stdc++.h>
using namespace std;

int sum_num[1000001];
int sums;
stack<int> st1,st2,re;
string bs;
int a,b;
int consult[1001000];
int strToInt(){
	int anss = 0;
	int len = bs.size();
	for(int i = 0;i<len;i++){
		anss*=10;
		anss+=(bs[i]-'0');
	}
	return anss;
}

void dfs(int pre,int next,int k){ 
    //前面已经分割出的和是pre,剩余可部分还有k位,next是剩余可分割的数字
	if(k<=0){
		if(pre <= a){	
			if(pre >= sums){
				sums = pre;
				sum_num[sums]++;
				st2 = st1;
				//清空re
				while(!re.empty()) {
					re.pop();
				}
				while(!st1.empty()){
					re.push(st1.top());
					st1.pop();
				}
				st1 = st2;
			}
		}
		return;
	}

	if(pre > a){
		return;
	}
	
	for(int i = 10;i<= pow(10,k);i*=10){
		if(pre+next%i <= a)
		{
			st1.push(next%i);
			dfs(pre+next%i,next/i,k-log(i)/log(10));
			st1.pop();
		}
			
	}
	
}

int main(){
	while(cin>>a>>bs){
		b = strToInt();
		if(!a && !b){
			break;
		}
		sums = -1;
		memset(sum_num,0,sizeof(sum_num));
		for(int i = 10;i<= pow(10,bs.size());i*=10){
			st1.push(b%i);
			dfs(b%i,b/i,bs.size()-log(i)/log(10));
			st1.pop();
		}
		
		if(sums==-1){ //没哟
			cout<<"error"<<endl;
		}else if(sum_num[sums] > 1){ //大于1个最优解
			cout<<"rejected"<<endl;
		}else{ //栈输出结果
			cout<<sums<<" ";
			stack<int> re2;
			while(!re.empty()){
				re2.push(re.top());
				re.pop();
			}
			while(!re2.empty()){
				cout<<re2.top()<<" ";
				re2.pop();
			}	
			cout<<endl;
		}

	}
}

3. 全排列问题

问题1:八皇后问题

总时间限制: 1000ms 内存限制: 65536kB

*描述
会下国际象棋的人都很清楚:皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。如何将8个皇后放在棋盘上(有8 * 8个方格),使它们谁也不能被吃掉!这就是著名的八皇后问题。 
对于某个满足要求的8皇后的摆放方法,定义一个皇后串a与之对应,即a=b1b2...b8,其中bi为相应摆法中第i行皇后所处的列数。已经知道8皇后问题一共有92组解(即92个不同的皇后串)。
给出一个数b,要求输出第b个串。串的比较是这样的:皇后串x置于皇后串y之前,当且仅当将x视为整数时比y小。

*输入
第1行是测试数据的组数n,后面跟着n行输入。每组测试数据占1行,包括一个正整数b(1 <= b <= 92)

*输出
输出有n行,每行输出对应一个输入。输出应是一个正整数,是对应于b的皇后串。

*样例输入
2
1
92

*样例输出
15863724
84136275

分析

  • 有限制的全排列问题,并且与顺序有关
  • 递归解决,从第一行开始放置,dfs(2)放第2行,直到第8行放完,故递归结束条件是dfs第9行
  • 其实一共四个约束:
    • 不能同一行:dfs自然解决
    • 不能同一列:设置标志数组记录每一列是否放置了
    • 左上-右下斜对角线不能同时放置:设置标志数组dex,dex[y-x+8]=1,y-x+8表示<x,y>所在的左上右下斜对角线的唯一标识
    • 右上-左下斜对角线不能同时放置:设置标志数组dex2,dex[x+y]=1,x+y表示<x,y>所在的右上-左下斜对角线的唯一标识
  • 本题打表

解决

#include<bits/stdc++.h> 
using namespace std;

int a[20][20];
int row[10];
int dex[20];
int dex2[20];
int n;
int numOfAns;

int result[100][10];

void dfs(int prex){
	if(prex > 8){
		//每找到一个结果,记录下来
		int cnt = 0;
		for(int i = 1;i<=8;i++){
			for(int j = 1;j<=8;j++){
				if(a[i][j] == 1){
					result[numOfAns][cnt++] = j;
					break;
				}
			}
		}
		numOfAns++;
		return;
	}

	//放第prex行
	for(int i = 1;i<=8;i++){
		if(row[i] == 0 && dex[i-prex+ 8] == 0 && dex2[i+prex] == 0){
			a[prex][i] = 1;
			dex[i-prex+8] = 1;
			row[i] = 1;	
			dex2[i+prex] = 1;
				
			dfs(prex+1);
			
			a[prex][i] = 0;
			dex[i-prex+8] = 0;
			row[i] = 0;	
			dex2[i+prex] = 0;
		}
	}
	
	
}

int main(){

	memset(row,0,sizeof(row));
	memset(a,0,sizeof(a));
	memset(dex,0,sizeof(dex));
	memset(dex2,0,sizeof(dex2));
	numOfAns = 0;
	for(int i = 1;i<=8;i++){ //第一行摆设的位置<1,i>
		a[1][i] = 1;
		dex[i-1+8] = 1;
		row[i] = 1;	
		dex2[i+1] = 1;
		
		dfs(2);
		
		a[1][i] = 0;
		dex[i-1+8] = 0;
		row[i] = 0;
		dex2[i+1] = 0;
	}
	int T;
	cin>>T;
	while(T--){
		cin>>n;
		for(int i = 0;i<8;i++){
			cout<<result[n-1][i];
		}
		cout<<endl;		
	}	
	return 0;
}

问题2:棋盘问题

虽然与N皇后类似,但还是有很多坑的,暴力每一层会超时。

再做做。

*描述
在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C。

*输入
输入含有多组测试数据。
每组数据的第一行是两个正整数,n k,用一个空格隔开,表示了将在一个n*n的矩阵内描述棋盘,以及摆放棋子的数目。 n <= 8 , k <= n
当为-1 -1时表示输入结束。
随后的n行描述了棋盘的形状:每行有n个字符,其中 # 表示棋盘区域, . 表示空白区域(数据保证不出现多余的空白行或者空白列)。

*输出
对于每一组数据,给出一行输出,输出摆放的方案数目C (数据保证C<2^31)。

*样例输入
2 1
#.
.#
4 4
...#
..#.
.#..
#...
-1 -1

*样例输出
2
1

分析

  • 相比于N皇后,少了斜对角线的条件
  • 此题dfs的终止不仅仅是超过边界,还要考已经放的棋子数
  • 不是每一层都要放,因此下一次递归dfs并不一定是从下一层开始,也有可能是从下下层开始,从下下层开始也就意味着下一层忽略,只要递归时不计下一层放的棋子数(+1)即可。当然也有可能从下下下层开始,但综合考虑,从本层开始,下一次递归只有两种可能,要么下一层,要么下下层,所谓下下下层即:本层开始,下层忽略,下下层也忽略,自然就从下下下层递归,依次类推。
  • 总而言之,从i层开始放旗子,下一次递归要么是:找到了i+1层某个位置,放棋子;要么是:从i+2层开始递归。

解决

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
char a[10][10];     //记录棋盘位置
bool book[10];        //记录一列是否已经放过棋子
int n,k;
int total;    //total 是放棋子的方案数 ,m是已放入棋盘的棋子数目

void DFS(int index,int dijigeshu)
{
    if(dijigeshu==k)
    {
        total++;
        return ;
    }
    if(index>k)    //边界
        return ;
    for(int j=0; j<n; j++)
    //判断条件,与八皇后相比这里多了判断是否为棋盘区的条件
        if(book[j]==0 && a[index][j]=='#')
        {
            book[j]=1;
            DFS(index+1,dijigeshu+1);//该行(第index行)找到了能放的位置并放入,到下一行
            book[j]=0;
        }
    DFS(index+1,dijigeshu);  //与八皇后的区别,该行没有符合条件的位置,直接到下一
}

int main()
{
    int i,j;
    while(scanf("%d%d",&n,&k)&&n!=-1&&k!=-1) //限制条件
    {
        total=0;
        for(i=0; i<n; i++)
            scanf("%s",&a[i]);
        memset(book,0,sizeof(book));
        DFS(0,0);
        printf("%d\n",total);
    }
    return 0;
}

4. 草丛问题

连通块个数、大小相关

问题1: 红与黑

广度优先搜索 - 最大连通点数

*描述
有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。你站在其中一块黑色的瓷砖上,只能向相邻的黑色瓷砖移动。请写一个程序,计算你总共能够到达多少块黑色的瓷砖。

*输入
包括多个数据集合。每个数据集合的第一行是两个整数W和H,分别表示x方向和y方向瓷砖的数量。W和H都不超过20。在接下来的H行中,每行包括W个字符。每个字符表示一块瓷砖的颜色,规则如下
1)‘.’:黑色的瓷砖;
2)‘#’:白色的瓷砖;
3)‘@’:黑色的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一次。
当在一行中读入的是两个零时,表示输入结束。

*输出
对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。

*样例输入
6 9 
....#. 
.....# 
...... 
...... 
...... 
...... 
...... 
#@...# 
.#..#. 
0 0

*样例输出
45

分析

  • BFS
  • 给定地图,图中最大连通点数
  • queue实现,记得边界检查

解决

#include<bits/stdc++.h>
using namespace std;

char maze[100][100];
int n,m;
int sti,stj;
int vis[100][100];
int main(){
	int ans = 0;
	while(scanf("%d%d",&m,&n) &&n&&m){
		for(int i = 0;i<n;i++){
			for(int j = 0;j<m;j++){
				cin>>maze[i][j];
				if(maze[i][j] == '@'){
					sti=i;
					stj=j;
				}
			}
		}
		memset(vis,0,sizeof(vis));
		ans = 0;
		queue<pair<int,int> > que;
		que.push(pair<int,int>(sti,stj));
		vis[sti][stj] = 1;
		while(!que.empty()){
			pair<int,int> p = que.front();
			ans++;
			int x = p.first;
			int y = p.second;
			que.pop();
			for(int i = -1;i<=1;i++){
				if(y+i>=0&&y+i<m && maze[x][y+i]=='.' && vis[x][y+i] == 0){
					vis[x][y+i] = 1;
					que.push(pair<int,int>(x,y+i));
				}
			}
			for(int i = -1;i<=1;i++){
				if(x+i>=0 && x+i < n &&maze[x+i][y]=='.' && vis[x+i][y] == 0){
					vis[x+i][y] = 1;
					que.push(pair<int,int>(x+i,y));
				}
			}		
		}
		cout<<ans<<endl;
		
	}
	return 0;
}

问题2:城堡问题

*描述

     1   2   3   4   5   6   7  
   #############################
 1 #   |   #   |   #   |   |   #
   #####---#####---#---#####---#
 2 #   #   |   #   #   #   #   #
   #---#####---#####---#####---#
 3 #   |   |   #   #   #   #   #
   #---#########---#####---#---#
 4 #   #   |   |   |   |   #   #
   #############################
           (图 1)

   #  = Wall   
   |  = No wall
   -  = No wall

图1是一个城堡的地形图。请你编写一个程序,计算城堡一共有多少房间,最大的房间有多大。城堡被分割成m*n(m≤50,n≤50)个方块,每个方块可以有0~4面墙。
输入
程序从标准输入设备读入数据。第一行是两个整数,分别是南北向、东西向的方块数。在接下来的输入行里,每个方块用一个数字(0≤p≤50)描述。用一个数字表示方块周围的墙,1表示西墙,2表示北墙,4表示东墙,8表示南墙。每个方块用代表其周围墙的数字之和表示。城堡的内墙被计算两次,方块(1,1)的南墙同时也是方块(2,1)的北墙。输入的数据保证城堡至少有两个房间。

*输出
城堡的房间数、城堡中最大房间所包括的方块数。结果显示在标准输出设备上。

*样例输入
4 
7 
11 6 11 6 3 10 6 
7 9 6 13 5 15 5 
1 10 12 7 13 7 5 
13 11 10 8 10 12 13 
    
*样例输出
5
9

分析

  • 连通块个数,并记录最大连通块
  • 每个数字代表墙的加和,可以用&(1<<i),i=1,2,3,4,来判断能不能该块能不能向四个方向走通
  • 第一种方法:BFS
  • 第二种方法:DFS
  • 第三种方法:并查集

解决

BFS

#include<bits/stdc++.h>
using namespace std;
int n,m;
int maze[1001][1001];
int vis[1001][1001];
typedef pair<int,int> P;
int main(){
	cin>>n>>m;
	for(int i = 0;i<n;i++){
		for(int j = 0;j<m;j++){
			cin>>maze[i][j];
		}
	}
	//先用bfs做
	int visnum = 0;
	queue<P> que;
	int maxnum = -1;
	int totalblock = 0;
	int flag = 0;
	memset(vis,0,sizeof(vis));
	for(int i = 0;i<n;i++) {
		for(int j=0;j<m;j++){
			if(!vis[i][j]){
				totalblock++; //块数+1
				vis[i][j] = 1;
				visnum++;
				que.push(P(i,j));
				int nownum = 0;
				while(!que.empty()){
					nownum++;
					P p = que.front();
					que.pop();
					int x = p.first;
					int y = p.second;
					if(y-1>=0 && !vis[x][y-1] && ((maze[x][y] & 1)==0)){
						vis[x][y-1] =1;
						visnum++;
						que.push(P(x,y-1));
					}
					if(y+1<m && !vis[x][y+1] && ((maze[x][y] & 4)==0)){
						vis[x][y+1] =1;
						visnum++;
						que.push(P(x,y+1));
					}					
					if(x-1>=0 && !vis[x-1][y] && ((maze[x][y] & 2)==0)){
						vis[x-1][y] =1;
						visnum++;
						que.push(P(x-1,y));
					}					
					if(x+1<n && !vis[x+1][y] && ((maze[x][y] & 8)==0)){
						vis[x+1][y] =1;
						visnum++;
						que.push(P(x+1,y));
					}				
				}
				maxnum = max(maxnum,nownum);
			}
		
			if(visnum == n*m){
				flag = 1;
				break;
			}
		}
		if(flag == 1){
			break;
		}
	}
	cout<<totalblock<<endl<<maxnum<<endl;
	return 0;
}

DFS

#include<cstdio>
#include<math.h>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
#include<map>
#define MAX 53
#define  num 32767
#define INF 0x7f7f7f
#define eps 1e-5
using namespace std;
int dx,nb;
int room=0,fk,maxfk;
int D[MAX][MAX];
bool visit[MAX][MAX]={0};
//这里四个方向能否走通需要额外判断
void dfs(int i, int j)
{
    //检查
    if(visit[i][j]) return;
    if(i<=0||j<=0||i>nb||j>dx) return ;
    //访问
    visit[i][j]=1;//必有
    fk++;
    //向四个方向延申
    if((D[i][j]&1)==0) dfs(i,j-1);//刚开始没给&运算加括号,==优先级高于按位与
    if((D[i][j]&2)==0) dfs(i-1,j);
    if((D[i][j]&4)==0) dfs(i,j+1);
    if((D[i][j]&8)==0) dfs(i+1,j);
}

int main()
{
   //freopen("input.txt","r",stdin);
    cin>>nb>>dx;
    for(int i=1;i<=nb;i++)
        for(int j=1;j<=dx;j++)
            cin>>D[i][j];
    memset(visit,sizeof(visit),0);
    for(int i=1;i<=nb;i++)
    {
        for(int j=1;j<=dx;j++)
        {
            if(!visit[i][j])
            {
                room++;
                fk=0;
                dfs(i,j);
                maxfk=max(maxfk,fk);
            }
        }
    }
    cout<<room<<endl<<maxfk<<endl;
   return 0;
}

5. 迷宫问题

路径、是否联通、路径最小长度等

问题1: 迷宫

问是否能走出迷宫?

*描述
一天Extense在森林里探险的时候不小心走入了一个迷宫,迷宫可以看成是由n * n的格点组成,每个格点只有2种状态,.和#,前者表示可以通行后者表示不能通行。同时当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。如果起点或者终点有一个不能通行(为#),则看成无法办到。

*输入
第1行是测试数据的组数k,后面跟着k组输入。每组测试数据的第1行是一个正整数n (1 <= n <= 100),表示迷宫的规模是n * n的。接下来是一个n * n的矩阵,矩阵中的元素为.或者#。再接下来一行是4个整数ha, la, hb, lb,描述A处在第ha行, 第la列,B处在第hb行, 第lb列。注意到ha, la, hb, lb全部是从0开始计数的。

*输出
k行,每行输出对应一个输入。能办到则输出“YES”,否则输出“NO”。

*样例输入
2
3
.##
..#
#..
0 0 2 2
5
.....
###.#
..#..
###..
...#.
0 0 4 0
    
*样例输出
YES
NO

分析

  • 简单连通性判断

解决

#include<bits/stdc++.h>
using namespace std;

char a[1001][1001];
int vis[1001][1001];
typedef pair<int,int> P;
int n,sx,sy,ex,ey;
int main(){
	int T;
	cin>>T;
	while(T--){
		cin>>n;
		for(int i = 0;i<n;i++){
			cin>>a[i];
		}
		cin>>sx>>sy>>ex>>ey;
		if(a[sx][sy] == '#' || a[ex][ey] == '#'){
			cout<<"NO"<<endl;
			continue;
		}
		memset(vis,0,sizeof(vis));
		queue<P> que;
		que.push(P(sx,sy));
		vis[sx][sy] = 1;
		while(!que.empty()){
			int x = que.front().first;
			int y = que.front().second;
			que.pop();
			for(int i = -1;i<=1;i++){
				if(x+i>=0 &&x+i<n &&!vis[x+i][y] &&a[x+i][y]=='.'){
					vis[x+i][y] = 1;
					que.push(P(x+i,y));
				}
				if(y+i>=0 &&y+i<n &&!vis[x][y+i] &&a[x][y+i]=='.'){
					vis[x][y+i] = 1;
					que.push(P(x,y+i));
				}				
			}
		}
		
		if(vis[ex][ey] == 1){
			cout<<"YES"<<endl;
		}else{
			cout<<"NO"<<endl;
		}
		
	} 
	return 0;
}

问题2: 走迷宫

走出迷宫的最短路径长度问题。

描述
一个迷宫由R行C列格子组成,有的格子里有障碍物,不能走;有的格子是空地,可以走。
给定一个迷宫,求从左上角走到右下角最少需要走多少步(数据保证一定能走到)。只能在水平方向或垂直方向走,不能斜着走。

输入
第一行是两个整数,R和C,代表迷宫的长和宽。( 1<= R,C <= 40)
接下来是R行,每行C个字符,代表整个迷宫。
空地格子用'.'表示,有障碍物的格子用'#'表示。
迷宫左上角和右下角都是'.'。

输出
输出从左上角走到右下角至少要经过多少步(即至少要经过多少个空地格子)。计算步数要包括起点和终点。

样例输入
5 5
..###
#....
#.#.#
#.#.#
#.#..

样例输出
9

分析

  • 增加“求最短步数”。
  • \(step[x][y]\)数组记录到<x,y>点的步数。

解决

#include<bits/stdc++.h>
using namespace std;

typedef pair<int,int> P;
int m,n;
char a[1001][1001];
int vis[1001][1001];
int step[1001][1001];
int main(){
	cin>>m>>n;
	for(int i = 0;i<m;i++){
		cin>>a[i];
	}
	memset(vis,0,sizeof(vis));
	memset(step,0,sizeof(step));
	
	queue<P> que;
	que.push(P(0,0));
	vis[0][0] = 1;
	step[0][0] = 1;
	while(!que.empty()){
		P p = que.front();
		que.pop();
		int x = p.first;
		int y = p.second;
		if(x == m-1 && y == n-1){
			break;
		}
		if(x+1<m && !vis[x+1][y] && a[x+1][y] == '.'){
			vis[x+1][y] = 1;
			step[x+1][y] = step[x][y]+1;
			que.push(P(x+1,y));
		}
		if(y+1<n && !vis[x][y+1] && a[x][y+1] == '.'){
			vis[x][y+1] = 1;
			step[x][y+1] = step[x][y]+1;
			que.push(P(x,y+1));
		}		
		if(x-1>=0 && !vis[x-1][y] && a[x-1][y] == '.'){
			vis[x-1][y] = 1;
			step[x-1][y] = step[x][y]+1;
			que.push(P(x-1,y));
		}		

		if(y-1>=0 && !vis[x][y-1] && a[x][y-1] == '.'){
			vis[x][y-1] = 1;
			step[x][y-1] = step[x][y]+1;
			que.push(P(x,y-1));
		}				
	}
	cout<<step[m-1][n-1]<<endl;
	return 0;
}

问题3: 还是迷宫问题

总时间限制: 1000ms 内存限制: 65536kB
    
描述
定义一个二维数组: 

int maze[5][5] = {

0, 1, 0, 0, 0,

0, 1, 0, 1, 0,

0, 0, 0, 0, 0,

0, 1, 1, 1, 0,

0, 0, 0, 1, 0,

};

它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线。



输入
一个5 × 5的二维数组,表示一个迷宫。数据保证有唯一解。

输出
左上角到右下角的最短路径,格式如样例所示。

样例输入
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
    
样例输出
(0, 0)
(1, 0)
(2, 0)
(2, 1)
(2, 2)
(2, 3)
(2, 4)
(3, 4)
(4, 4)

**分析*8

  • 记录路径
  • 使用\(pair<int,int>\)类型的二维数组记录前驱:\(pair<int,int> pre[100][100]\)

解决

#include<bits/stdc++.h>
using namespace std;

int a[10][10];
int vis[10][10];
typedef pair<int,int > P;
//int prex[100][100];
//int prey[100][100];
P pre[100][100];
int main(){
	
	for(int i = 0;i<5;i++){
		for(int j = 0;j<5;j++){
			cin>>a[i][j];
//			prex[i][j] = -1;
//			prey[i][j] = -1;
			pre[i][j].first = -1;
			pre[i][j].second = -1;	
			vis[i][j] = 0;	
		}
	}
	memset(vis,0,sizeof(vis));	
	queue<P> que;
	que.push(P(0,0));

	pre[0][0] = P(-1,-1);

	vis[0][0] = 1;
	while(!que.empty()){
		P p = que.front();
		int x = p.first;
		int y = p.second;
		que.pop();
		if(x == 4 && y== 4){
			break;
		}
		if(x+1<5 && !vis[x+1][y] && a[x+1][y] == 0){
			vis[x+1][y] = 1;
			pre[x+1][y] = P(x,y);
			que.push(P(x+1,y));
			if(x == 4 && y== 4){
					break;
				}			
		}
		if(y+1<5 && !vis[x][y+1] && a[x][y+1] == 0){
			vis[x][y+1] = 1;
			pre[x][y+1] = P(x,y);
			que.push(P(x,y+1));
			if(x == 4 && y== 4){
					break;
				}			
		}		
		if(x-1>=0 && !vis[x-1][y] && a[x-1][y] == 0){
			vis[x-1][y] = 1;
			pre[x-1][y] = P(x,y);	
			que.push(P(x-1,y));
			if(x == 4 && y== 4){
					break;
				}			
		}		

		if(y-1>=0 && !vis[x][y-1] && a[x][y-1] == 0){
			vis[x][y-1] = 1;
			pre[x][y-1] = P(x,y);	
			que.push(P(x,y-1));
			if(x == 4 && y== 4){
					break;
				}			
		}	
	}
	
	stack<P> st;
	int x1=4,x2=4;
	st.push(P(x1,x2));
	while(pre[x1][x2].first>=0&&pre[x1][x2].second>=0){
		st.push(P(pre[x1][x2].first,pre[x1][x2].second));
		
		int x11 = pre[x1][x2].first;
		int x22 = pre[x1][x2].second;
		x1 = x11;
		x2 = x22;
	}
	while(!st.empty()){
		cout<<"("<<st.top().first<<", "<<st.top().second<<")"<<endl;
		st.pop();
	}
	return 0;
}

6. 广度优先搜索的剪枝

题目 游戏
时间: 1.0s
内存: 256.0MB
问题描述: 小明在玩一个电脑游戏,游戏在一个n×m的方格图上进行,小明控制的角色开始的时候站在第一行第一列,目标是前往第n行第m列。方格图上有一些方格是始终安全的,有一些在一段时间是危险的,如果小明控制的角色到达一个方格的时候方格是危险的,则小明输掉了游戏,如果小明的角色到达了第n行第m列,则小明过关。第一行第一列和第n行第m列永远都是安全的。每个单位时间,小明的角色必须向上下左右四个方向相邻的方格中的一个移动一格。经过很多次尝试,小明掌握了方格图的安全和危险的规律:每一个方格出现危险的时间一定是连续的。并且,小明还掌握了每个方格在哪段时间是危险的。现在,小明想知道,自己最快经过几个时间单位可以达到第n行第m列过关。
输入: 输入的第一行包含三个整数n, m, t,用一个空格分隔,表示方格图的行数n、列数m,以及方格图中有危险的方格数量。接下来t行,每行4个整数r, c, a, b,表示第r行第c列的方格在第a个时刻到第b个时刻之间是危险的,包括ab。游戏开始时的时刻为0。输入数据保证rc不同时为1,而且当rnc不为m。一个方格只有一段时间是危险的(或者说不会出现两行拥有相同的rc)。
输出: 输出一个整数,表示小明最快经过几个时间单位可以过关。输入数据保证小明一定可以过关。样例输入:3 3 3 2 1 1 1 1 3 2 10 2 2 2 10 样例输出:6。样例说明:第2行第1列时刻1是危险的,因此第一步必须走到第1行第2列。第二步可以走到第1行第1列,第三步走到第2行第1列,后面经过第3行第1列、第3行第2列到达第3行第3列。
评测用例规模与约定: 前30%的评测用例满足:0 < n, m ≤ 10,0 ≤ t < 99。所有评测用例满足:0 < n, m ≤ 100,0 ≤ t < 9999,1 ≤ r

分析

  • 毫无疑问是bfs,用队列

  • 用struct存储二维点,比用pair进出队列要快!

  • 剪枝:vis数组标记某个点是否走过。但这里有个坑,并不是走过的就不能走了,因为某点的毒的时间是一个区间,可能必须绕回到某点来躲避毒。所以vis标记的应该是同一时刻,某个点不能重复到达!这个是剪枝的坑,即同一时刻到达过某点,若再绕回到某点,则不能理他。所以vis是三维的,\(vis[x][y][t]=1\)表示在t时刻,(x,y)点已经访问过了。

  • 这种可能超时的题目,能不调用函数就不调用。调用额外花很多时间!

解决

#include<bits/stdc++.h>
using namespace std;
struct node{
	int x,y,time;
	node(int xx,int yy,int t):x(xx),y(yy),time(t){}
};

int vis[400][300][300];
int n,m,t;
int dis[4][2] = {1,0,0,1,-1,0,0,-1};
int l[104][104];
int r[104][104];
int main(){
	cin>>n>>m>>t;
	memset(vis,0,sizeof(vis));
	queue<node> que;
	
	que.push(node(1,1,0));
	vis[1][1][0] = 1;
	for(int i = 0;i<t;i++){
		int a,b;
		cin>>a>>b;
		cin>>l[a][b]>>r[a][b];
	}
	int ans = 0;
	while(!que.empty()){
		node no = que.front();
		que.pop();
		if(no.x == n && no.y == m){
			ans = no.time;
			break;
		}
		for(int i = 0;i<4;i++){
				int x = no.x+dis[i][0];
				int y = no.y+dis[i][1];
				int t = no.time;
				if(x>=1&&x<=n&&y>=1&&y<=m && vis[x][y][t+1]==0){ //剪枝
					//看是否没毒
					if((l[x][y]==0 && r[x][y] ==0 )) {
						vis[x][y][t+1] = 1;
						que.push(node(x,y,t+1));
					}else if(t+1>=l[x][y] && t+1 <=r[x][y]){
						continue;
					}else{
						vis[x][y][t+1] =1;
						que.push(node(x,y,t+1));	
					}
				}				
		}
	}
		
	cout<<ans<<endl;	

	return 0;
}

7. 总操作步数固定枚举问题

练习这个题。

问题1: 算24

总时间限制: 3000ms 内存限制: 65536kB

*描述
给出4个小于10个正整数,你可以使用加减乘除4种运算以及括号把这4个数连接起来得到一个表达式。现在的问题是,是否存在一种方式使得得到的表达式的结果等于24。

这里加减乘除以及括号的运算结果和运算的优先级跟我们平常的定义一致(这里的除法定义是实数除法)。

比如,对于5,5,5,1,我们知道5 * (5 – 1 / 5) = 24,因此可以得到24。又比如,对于1,1,4,2,我们怎么都不能得到24。

*输入
输入数据包括多行,每行给出一组测试数据,包括4个小于10个正整数。最后一组测试数据中包括4个0,表示输入的结束,这组数据不用处理。

*输出
对于每一组测试数据,输出一行,如果可以得到24,输出“YES”;否则,输出“NO”。

*样例输入
5 5 5 1
1 1 4 2
0 0 0 0
    
*样例输出
YES
NO

分析

  • 总步数一定,也就是操作4次,每次任选两个数进行计算
  • 不能\(dfs(pre,step)\),因为这样实际上枚举的次数不够,上次的计算结果pre一定参与进行下一次运算,但这是不对的,比如可能前两个数运算,后两个数运算,然后再运算。
  • 正确的思路是:dfs步数,每一步任选两个数,vis控制选过没有,将运算结果放置在a[i]中,一轮dfs结束恢复a[i]为原数。

解决

#include<bits/stdc++.h>
#define eps 1e-5
using namespace std;


double a[10];
int vis[10];
int flag;
int sign(double x){
	if(fabs(x) < eps){
		return 0;
	}
	if(x > eps) return 1;
	else return -1;
}
void dfs(int k){
	if(k == 4){
		for(int i = 0;i<4;i++){
			if(vis[i] == 0){
				if(sign(a[i] - 24.0) == 0){
					flag = 1;
					break;
				}
			}
		}
		return;
	}
	
	for(int i = 0;i<4;i++){
		if(!vis[i]){
			for(int j = i+1;j<4;j++){
				if(!vis[j]){
					vis[j] = 1;
					
					double x = a[i];
					double y = a[j];
                    
					//六个方向dfs
					a[i] = x+y;dfs(k+1);
					a[i] = x-y;dfs(k+1);
					a[i] = y-x;dfs(k+1);
					a[i] = x*y;dfs(k+1);
					if(sign(y)!=0) a[i] = x/y;dfs(k+1);
					if(sign(x)!=0) a[i] = y/x;dfs(k+1);					
					
					//恢复
					a[i] = x;
					a[j] = y;
					vis[j] = 0;				 
				}
			}			
		}
	}
}

int main(){
	while(cin>>a[0]>>a[1]>>a[2]>>a[3]){
		if(sign(a[0]+a[1]+a[2]+a[3]) == 0){
			break;
		}
		memset(vis,0,sizeof(vis));
		flag = 0;
		dfs(1);
		if(flag)
			cout<<"YES"<<endl;
		else
			cout<<"NO"<<endl;
	}
	return 0;
}

四、数学问题

1. 高精度计算

问题1: 大数加法

#include<bits/stdc++.h>
#define MAX 100101
using namespace std;
//输入cin>> char a[MAX] char b[MAX] 定义 result[MAX+10]足够了
char* add(char*a,char*b){
	
	int aa[MAX],bb[MAX],temp[MAX*2+10];
	char result[MAX*2+10];
	memset(aa,0,sizeof(aa));
	memset(bb,0,sizeof(bb));
	memset(temp,0,sizeof(temp));
	memset(result,0,sizeof(result));
			
	int lena = strlen(a);
	int lenb = strlen(b);
	
	//注意把顺序倒过来 
	for(int i =0;i<lena;i++){
		aa[i] = a[lena-i-1] - '0';
	}
	for(int j = 0;j<lenb;j++){
		bb[j] = b[lenb-j-1] - '0';
	}
	
	for(int i = 0;i<max(lena,lenb);i++){
		temp[i] = aa[i]+bb[i];
	}
	
	int c = 0; //进位
	//注意最多max(lena,lenb)+1位 
	for(int i = 0;i< max(lena,lenb) + 1;i++) {
		int tmpx = c + temp[i];
		temp[i] = tmpx%10;
	    c = tmpx/10;
	}
	
	//去除前导0 
	int ind = max(lena,lenb);
	while(temp[ind] == 0 && ind>=0) ind--; 
	if(ind<0) result[0] = '0';//全是0
	else{
		for(int i = 0;i<=ind;i++){
			result[ind-i] = temp[i] + '0';
		}
		result[ind+1] = '\0';
	}
	return result;
}

int main(){
	char a[MAX];
	char b[MAX];
	scanf("%s%s",a,b);
	char* result = add(a,b);
	cout<<result<<endl;
	return 0;
}

问题2: 大数乘法

#include<bits/stdc++.h>
#define MAX 10010
using namespace std;

char result[MAX*2+10];

void Multipy(char*a,char*b){
	int aa[MAX],bb[MAX],temp[MAX];
	int lena = strlen(a),lenb = strlen(b);
	memset(aa,0,sizeof(aa));
	memset(bb,0,sizeof(bb));
	memset(temp,0,sizeof(temp));
	
	for(int i = 0;i<lena;i++){
		aa[i] = a[lena-1-i] - '0';
	}
	for(int i = 0;i<lenb;i++){
		bb[i] = b[lenb-1-i] - '0';
	}
	//双重循环求和,注意temp下标!temp[i+j]
	for(int i = 0;i<lena;i++){
		for(int j = 0;j<lenb;j++){
			temp[i+j] += (aa[i]*bb[j]);
		}
	}
	//求结果,注意范围,最多有lena+lenb位
	int c = 0; //进位 
	for(int i = 0;i<lena+lenb+1;i++){
		int mid = temp[i] + c;
		temp[i] = mid % 10;
		c = mid / 10;
	}
	//去除前导0
	int ind = lena+lenb;
	while(temp[ind]==0 && ind>=0){
		ind--;
	}
	if(ind < 0){
		result[0] = '0';
		result[1] = '\0';
	}else{	
		for(int i = 0;i<=ind;i++){
			result[ind-i] = temp[i] + '0';
		}
		result[ind+1] = '\0';
	}
}
int main(){
	char a[MAX];
	char b[MAX];
	scanf("%s%s",a,b);
	Multipy(a,b);
	cout<<result<<endl;

	return 0;
}

问题3: 2的N次方

描述
任意给定一个正整数N(N<=100),计算2的n次方的值。

输入
输入一个正整数N。

输出
输出2的N次方的值。

样例输入
5

样例输出
32

解决

直接用long double可以过

#include<bits/stdc++.h>
using namespace std;

int main(){
	long double x;
	int n;
	cin>>n;
	x = pow(2,n);
	printf("%.0Lf\n",x);
	return 0;
}

高精度乘法运算

为啥结果一样,但过不了?

#include<bits/stdc++.h>
#define MAX 100
using namespace std;

char result[100];
//char result2[1001];

void Multipy(char*a,char*b){
	int aa[MAX],bb[MAX],temp[MAX];
	int lena = strlen(a),lenb = strlen(b);
	memset(aa,0,sizeof(aa));
	memset(bb,0,sizeof(bb));
	memset(temp,0,sizeof(temp));
	
	for(int i = 0;i<lena;i++){
		aa[i] = a[lena-1-i] - '0';
	}
	for(int i = 0;i<lenb;i++){
		bb[i] = b[lenb-1-i] - '0';
	}
	//双重循环求和,注意temp下标 
	for(int i = 0;i<lena;i++){
		for(int j = 0;j<lenb;j++){
			temp[i+j] += (aa[i]*bb[j]);
		}
	}
	//求结果,注意范围,最多有lena+lenb位
	int c = 0; //进位 
	for(int i = 0;i<lena+lenb+1;i++){
		int mid = temp[i] + c;
		temp[i] = mid % 10;
		c = mid / 10;
	}
	//去除前导0
	int ind = lena+lenb;
	while(temp[ind]==0 && ind>=0){
		ind--;
	}
	if(ind < 0){
		result[0] = '0';
		result[1] = '\0';
	}else{	
		for(int i = 0;i<=ind;i++){
			result[ind-i] = temp[i] + '0';
		}
		result[ind+1] = '\0';
	}
}
int main(){
	while(1){
		memset(result,0,sizeof(result));
		int a;
		cin>>a;
		char b[2];
		b[0] = '2';
		result[0] = '1';
		for(int i = 0;i<a;i++){
			Multipy(result,b);
		//	cout<<result<<endl;
		}
			
		cout<<result<<"---"<<endl;
		
		long double x;
		x = pow(2,a);
		
		printf("%.0Lf\n",x);		
	}
	
	return 0;
}

问题3:浮点数的大数加法

分析

  • 大数加法
  • 注意小数点,对齐小数点
  • 去除前导0、还要记得去除后导0
  • 输出再添加上小数点

解决

#include<bits/stdc++.h>
using namespace std;
char result[10010];
void add(char *a,char*b){
	int aa[105],bb[105],temp[205];
	memset(aa,0,sizeof(aa));
	memset(bb,0,sizeof(bb));
	memset(temp,0,sizeof(temp));
	
	int lena = strlen(a);
	int lenb = strlen(b);
	//先反转 
	for(int i = 0;i<lena;i++){
		aa[lena-1-i] = a[i] - '0';
	}
	for(int i = 0;i<lenb;i++){
		bb[lenb-1-i] = b[i] - '0';
	}
	//找到point小数点
	int pointa = -1,pointb = -1;
	for(int i = 0;i<lena;i++){
		if(aa[i] + '0' == '.'){
			pointa = i;
			break;
		}
	}
	for(int i = 0;i<lenb;i++){
		if(bb[i] + '0' == '.'){
			pointb = i;
			break;
		}
	} 
	//小数点对齐 
	if(pointa > pointb){
		int ab = pointa - pointb;
		for(int i = lenb-1+ab;i>=1;i--){
			bb[i] = bb[i-ab];
		}
		lenb = lenb + ab; //更新 
		//bb[0] = 0;
		for(int i = 0;i<ab;i++){
			bb[i] = 0;
		}
	}
	if(pointb > pointa){
		int ab = pointb - pointa;
		for(int i = lena-1+ab;i>=1;i--){
			aa[i] = aa[i-ab];
		}
		lena = lena+ab; //更新 
		for(int i = 0;i<ab;i++) {
			aa[i] = 0;			
		}
	}
	//去掉小数点
	int point = max(pointa,pointb);
	for(int i = point;i<lena-1;i++){
		aa[i] = aa[i+1];
	}
	aa[lena-1] = 0;
	lena--;
	for(int i = point;i<lenb-1;i++){
		bb[i] = bb[i+1];
	}
	bb[lenb-1] = 0;
	lenb--;	
	//相加
	for(int i = 0;i<max(lena,lenb)+1;i++) {
		temp[i] = aa[i]+bb[i];
	}
	int cnt = 0; //进位
	for(int i = 0;i<max(lena,lenb)+2;i++) {
		int mid = temp[i] + cnt;
		temp[i] = mid % 10;
		cnt = mid / 10;
	}
	//添加小数点 point位置
	for(int i = max(lena,lenb)+1;i>point;i--) {
		temp[i] = temp[i-1];
	}
	temp[point] = '.'-'0';
	//去除前导0
	int ind = max(lena,lenb)+1;
	while(temp[ind] == 0) ind--;
	//去除后导0
	int fnd = 0;
	while(temp[fnd] == 0)  fnd++;
	
	if(ind < 0){
		result[0] = '0';
		result[1] = '\0';
	}else{
		if(temp[ind] == '.' - '0'){
			ind++;
		}
		int cntt = 0;
		for(int i = fnd;i<=ind;i++){
			result[cntt] = temp[ind-cntt] + '0';
			cntt++;
		}
		result[ind+1] = '\0';
	}
}

int main(){
	
	char a[1000];
	char b[1000];
	cin>>a>>b;
	add(a,b); 
	printf("%s",result);
        
	return 0;
}

2. 素数和质因子分解

问题1: 素数 - 欧拉筛法

用于筛选出某一范围内的素数,常先打表。欧拉筛法快于艾氏筛法,其核心是:每次测试数 i 时,筛选掉小于等于 i 的素数同 i 的乘积构成的合数。

  • 这个容易忘记,多看一下
#include<bits/stdc++.h>
using namespace std;

int isprime[10010];
int prime[10010];
void getPrime(int n){

	fill(isprime,isprime+n,1);
	isprime[0] = isprime[1] = 0;
	int cnt = 0;
	for(int i = 2;i<=n;i++){
		if(isprime[i]){
			prime[cnt++] = i;
		}
		
		for(int j = 0;j<cnt&&i*prime[j]<=n;j++){
			isprime[i*prime[j]] = 0;
		}
	}
	//最后:素数表是prime, 个数是cnt 
	
}

int main(){
	getPrime(10000);
	for(int i = 0;i<100;i++){
		printf("%d ",prime[i]);
	}
	return 0;
}

问题2: 质因子分解

如: \(180 = 2^2 * 3^2*5^1\),每个合数都可以写成几个质数相乘的形式,其中每个质数都是这个合数的因数。

#include<bits/stdc++.h>
using namespace std;

//先建立素数表
int isprime[10010];
int prime[10010];
int p_index;
struct node{
	int x; //因子 
	int e; //指数 
}Node[1001];

void getPrime(int n){
	fill(isprime,isprime+n,1);
	p_index = 0;
	isprime[0] = isprime[1] = 0;
	for(int i = 2;i<=n;i++){
		if(isprime[i]){
			prime[p_index++] = i;
		}		
		for(int j = 0;j<p_index && i*prime[j]<=n;j++){
			isprime[i*prime[j]] = 0;
		}
	}
}

int main(){
	
	int n,n_tmp;
	cin>>n;
	n_tmp = n;
    //获取素数表
	getPrime(n/2+1);
 	//遍历所有2-n/2素数 
	int cnt = 0;//项数下标
	for(int i = 0;i<p_index;i++){
		if(n%prime[i] == 0){
			Node[cnt].x = prime[i];
			Node[cnt].e = 0;
			//找指数
			while(n%prime[i] == 0) {
				Node[cnt].e ++;
				n /= prime[i];
			}
			cnt++;
		}
	}
	
	if(n>1){ //说明n是个质数
		Node[0].x = n_tmp; 
		Node[0].e = 1;
		cnt = 1;
	}
	
	//打印结果
	cout<<n_tmp<<" = ";
	for(int i = 0;i<cnt-1;i++) {
		cout<<Node[i].x<<"^"<<Node[i].e<<" * ";
	}
	cout<<Node[cnt-1].x<<"^"<<Node[cnt-1].e<<endl;	
	return 0;
}

3. 欧几里得和扩展欧几里得算法

问题1: 欧几里得算法

即辗转相除法求最大公约数。利用的性质是:\(gcd(a,b)=gcd(b,a\%b)\),而\(gcd(a,0)=a\),这样,辗转除下去,当第二个参数为0,第一个参数就是最大公约数。

#include<bits/stdc++.h>
using namespace std;
int gcd(int a,int b){
	while(b!=0){
		int tmp = a%b;
		a =  b;
		b = tmp;
	}
	return a;
}
int main(){	
	int a,b;
	cin>>a>>b;
	int c = gcd(a,b);
	cout<<c;
	return 0;	
}

问题2: 扩展欧几里得算法

EcGcd算法: 扩展欧几里得算法不仅可以用来求最大公约数,还可以求逆元满足ax+by=gcd(a,b)的x和y。

基于的原理是:ax+by=gcd(a,b)一定存在解(x,y)。

一个用处就是:问ax+by=1是否有解,就是看a,b是否互质,即gcd(a,b)=1。

求满足ax+by=gcd(a,b)的(x,y)的过程,就是证明ax+by=gcd(a,b)一定有解的过程。

\(ax+by=gcd(a,b)=gcd(b,a\%b)=bx_2+a\%by_2\)

\(a\%b=a-a/b*b\)

因此 \(ax+by =bx_2+(a-a/b*b)y_2 =ay_2+b(x_2-a/b*y2)\)

从而 \(;x = y2;y=x_2-a/by_2\)

层层递归下去,最终当\(b=0\)时,返回\(gcd(a,b)=a\),此时有一组解\(x=1,y=0\),回溯,利用上式求出\(x,y\)

从而,这就建立了要求的(x,y)和前一状态的关系,算法的递归实现如下:

#include<bits/stdc++.h>
using namespace std;


int exgcd(int a,int b,int& x,int& y){
	if(b == 0){
		x = 1;
		y = 0;
		return a;
	}
	int gcd = exgcd(b,a%b,x,y);
	int tmp = x;
	x = y;
	y = tmp - a/b*y;
	
	return gcd;
}

int main(){
	
	int a,b;
	cin>>a>>b;
	int x,y;
	int gcd = exgcd(a,b,x,y);
	cout<<a<<" * ("<<x<<") + " <<b <<" * ("<<y<<") = "<<gcd<<endl;
	return 0;
}

问题3: 求最小逆元

  • 逆元:\(ax == 1 mod(m)\),则称\(x\)\(a\)关于\(m\)的逆元。
  • 最小逆元:满足上式的结果\(x\)可不止一个,往往要求的是最小的(且正)。
  • 求:\(ax==1mod(m)\) \(=>\) \(ax+my=1\)有解,求此式的解中最小的\(x\)即可。即求出此式\(x\)\(x=x\%m\),如果\(x<0\)\(x+=m\)
  • \(ax+my=1\)有解的条件是\(gcd(a,m)=1\)

求解代码借助exgcd。

//求最小逆元
#include<bits/stdc++.h>
using namespace std;
int exgcd(int a,int b,int &x,int &y){
	if(b == 0){
		x = 1;
		y = 0;
		return a;
	}
	int gcd = exgcd(b,a%b,x,y);
	int tmp = x;
	x = y;
	y = tmp - a/b * y; //这个公式会推 
	return gcd;
}

int cal(int a,int m){
	//求a关于m的逆元
	//ax+my=1  ax+by=1有解的条件是:gcd(a,b)=1 
	int x,y;
	int gcd = exgcd(a,m,x,y);
	if(gcd != 1) return -1; //逆元不存在 
	x = x % m;
	if(x < 0) x = x + m;
	return x;
}
int main(){
	int a,m;
	cin>>a>>m;
	cout<<"逆元:"<<cal(a,m)<<endl;
	return 0;
}

问题4: 青蛙约会

挺好的一题。占坑

4. 进制转换问题

问题1: 十进制转R进制

#include<bits/stdc++.h>
using namespace std;

void  tenToR(int a,int r, char* result){
	
	int cnt = 0;
	while(a){
		result[cnt++] = a % r + '0';
		a /= r;
	}
	if(r == 16){
		for(int i = 0;i<cnt;i++){
			if(result[i] == 10+'0'){
				result[i] = 'A' + '0';
			}else if(result[i] == 11+'0'){
				result[i] = 'B' + '0';
			}else if(result[i] == 12+'0'){
				result[i] = 'C' + '0';
			}else if(result[i] == 13+'0'){
				result[i] = 'D' + '0';
			}else if(result[i] == 14+'0'){
				result[i] = 'E' + '0';
			}else if(result[i] == 15+'0'){
				result[i] = 'F' + '0';
			}
		}		
	}
	result[cnt] = '\0';
	//cout<<resul<<endl;
	for(int i = 0;i<cnt;i++){
		//cout<<result[i] - '0'<<endl;
		//printf("%c",result[i]-'0');
		result[i] = result[i] - '0';
	}
}

int main(){
	
	int a;
	int r;
	cin>>a>>r;
	
	char result[100];
	tenToR(a,r,result);
	cout<<result<<endl;
	return 0;
}

问题2: R进制转10进制

#include<bits/stdc++.h>
using namespace std;
int RtoTen(char* a,int r){
	int len = strlen(a);
	int result = 0;
	for(int i = 0;i<len;i++){
		result*=r;
		result+=a[i]-'0';
	}
	return result;
}

int main(){
	char a[100];
	int r; //r进制
	cin>>a;
	cin>>r;
	cout<<RtoTen(a,r);
	return 0;
}

5. 日期计算问题

问题1: 两日期天数差

  • 已知两日期,求中间间隔天数
  • 日期格式化输入
#include<bits/stdc++.h>
using namespace std;

int month[2][13]; //第一行12个数为不是闰年的12个月的天数 

bool isLeapYear(int y){ //判断是否是闰年
	if(y % 400 == 0)  return 1;
	if(y % 4 == 0 && y % 100 != 0)   return 1;
	return 0;
}

void getMonthDay(){
	for(int i = 0;i<=1;i++){
		month[i][1] = 31;
		month[i][2] = 28;
		month[i][3] = 31;
		month[i][4] = 30;
		month[i][5] = 31;
		month[i][6] = 30;
		month[i][7] = 31;
		month[i][8] = 31;
		month[i][9] = 30;
		month[i][10] = 31;
		month[i][11] = 30;
		month[i][12] = 31;						
	}
	month[1][2] = 29;
}

int getDays(int year1,int m1,int d1,int year2,int m2,int d2){
	
	//先求年
	int days = 0;
	if(year1+1 < year2)
		for(int i = year1 +1;i<year2;i++){
			int leap = isLeapYear(i);
			for(int j = 1;j<=12;j++)
				days += month[leap][j];
		}
	
	if(year1+1 <= year2){
		//再求剩下的月
		for(int i = m1;i<=12;i++) {
			days += month[isLeapYear(year1)][i];
		}
		
		for(int i = 1;i<=m2;i++){
			days += month[isLeapYear(year2)][i];
		}
		
		//去掉多余的
		days -= d1;
		days -= (month[isLeapYear(year2)][m2] - d2);		
	}else{
		for(int i = m1;i<=m2;i++){
			days += month[isLeapYear(year1)][i];	
		}
		days -= d1;
		days -= (month[isLeapYear(year2)][m2] - d2);				
	}

	return days;
}

int main(){
	getMonthDay();
	int y1,m1,d1,y2,m2,d2;
	cin>>y1>>m1>>d1>>y2>>m2>>d2;
	cout<<getDays(y1,m1,d1,y2,m2,d2);
	return 0;
}

问题2: 计算下一个日期

  • 已知该日期以及间隔时间,求下一个日期
  • 实际情况根据题目要求,求出的日期可能减一
  • 没有进行日期标准化
#include<bits/stdc++.h>
using namespace std;
int month[2][13]; //第一行12个数为不是闰年的12个月的天数 

bool isLeapYear(int y){ //判断是否是闰年
	if(y % 400 == 0)  return 1;
	if(y % 4 == 0 && y % 100 != 0)   return 1;
	return 0;
}
void getMonthDay(){
	for(int i = 0;i<=1;i++){
		month[i][1] = 31;
		month[i][2] = 28;
		month[i][3] = 31;
		month[i][4] = 30;
		month[i][5] = 31;
		month[i][6] = 30;
		month[i][7] = 31;
		month[i][8] = 31;
		month[i][9] = 30;
		month[i][10] = 31;
		month[i][11] = 30;
		month[i][12] = 31;						
	}
	month[1][2] = 29;
}
void getNextDay(int year1,int m1,int d1,int days,int &year2,int &m2,int &d2){
	while(days--){
		d1++;
		if(d1 == month[isLeapYear(year1)][m1]){
			d1 = 1;
			m1 ++;
		}
		if(m1 == 12 + 1){
			m1 = 1;
			year1 ++;
		}
	}
	year2 = year1;
	m2 = m1;
	d2 = d1;
}
int main(){
	
	getMonthDay();
	int y1,m1,d1,y2,m2,d2,days;
	cin>>y1>>m1>>d1>>days;
	getNextDay(y1,m1,d1,days,y2,m2,d2);
	cout<<y2<<"-"<<m2<<"-"<<d2<<endl;
	return 0;
}

6. 二分法和排序

问题1: STL的二分函数

#include <algorithm>

//sort排序
int a[MAX];//对已经排好序的数组直接二分可以使用stl

//升序数组返回第一个大于等于num的指针
lower_bound(a,a+MAX,num);

//升序数组返回第一个大于等于num的下标
lower_bound(a,a+MAX,num)-a;

//升序数组返回第一个大于num的指针
upper_bound(a,a+MAX,num);

//升序数组返回第一个大于num的下标
upper_bound(a,a+MAX,num)-a;

//升序数组返回num的个数
upper_bound(a,a+MAX,num)-lower_bound(a,a+MAX,num);

//降序数组返回第一个小于等于num的指针
lower_bound(a,a+MAX,num,greater<int>() );

问题2: 二分模板

浮点数

//要求解空间和要求的结果有线性关系(单增单减)
//定义精度 #define eps 1e-5
double solve(double L,double R)
{
    double left = L,right = R,mid;
    while(right-left>eps)//注意精度
    {
        mid = left+(right-left)/2.0;
        if(结果大了) right =mid;//右不缩进
        else left = mid;//做不缩进
    }
    return mid;
}
//原文:https://blog.csdn.net/zongza/article/details/80908964

整数

//1 二分查找区间内某个数字的下标(存在且唯一),不存在返回-1:
int search(int l,int r,int x)
{
    int mid;
    while (l<=r)//唯一一个需要带=的二分
    {
        mid=(l+r)>>1;
        if(a[mid]==x) return mid;//三分支存在且唯一
        if(a[mid]<x) l=mid+1;//左缩进
        else r=mid-1;//右缩进
    }
    return -1;
}

//2 查询区间内<=x的最大值(有多个最大值时返回最靠右的坐标):
int search(int l,int r,int x)
{
    int mid;
    while (l<r)
    {
        mid=(l+r+1)>>1;
        if(a[mid]<=x) l=mid; //左不缩进,如果查询<x最大值直接改成<即可
        else r=mid-1;//(因为是返回靠右的坐标)右缩进
    }
    return l;
}

//3 查询区间内>=x的最小值(有多个最小值时返回最靠左的坐标)
int search(int l,int r,int x)
{
    int mid;
    while (l<r)
    {
        mid=(l+r)>>1;
        if(a[mid]>=x) r=mid;//右不缩进
        else l=mid+1;//(因为是返回靠左的坐标)左缩进
    }
    return l;
}

问题3: 木材加工问题

总时间限制: 1000ms 内存限制: 65536kB

描述
木材厂有一些原木,现在想把这些木头切割成一些长度相同的小段木头,需要得到的小段的数目是给定了。当然,我们希望得到的小段越长越好,你的任务是计算能够得到的小段木头的最大长度。
木头长度的单位是厘米。原木的长度都是正整数,我们要求切割得到的小段木头的长度也要求是正整数。

输入
第一行是两个正整数N和K(1 ≤ N ≤ 10000, 1 ≤ K ≤ 10000),N是原木的数目,K是需要得到的小段的数目。
接下来的N行,每行有一个1到10000之间的正整数,表示一根原木的长度。
 
输出
输出能够切割得到的小段的最大长度。如果连1厘米长的小段都切不出来,输出"0"。

样例输入
3 7
232
124
456
    
样例输出
114
#include<bits/stdc++.h> 
using namespace std;
int a[10010];
int main(){
	int n,k;
	cin>>n>>k;
	int sum = 0;
	for(int i = 0;i<n;i++){
		cin>>a[i];
		sum+=a[i];
	}
	if(sum < k){
		cout<<"0"<<endl;
	}
	else if(sum == k){
		cout<<"1"<<endl;
	}
	else{
		int l = 1,r = sum/k;
		int maxlen = 0;
		while(l<r){
			int mid = (l+r)/2;
			//cout<<l<<"--" <<r<<"--" <<mid<<endl;
			int sumNode = 0;
			for(int i = 0;i<n;i++){
				sumNode += a[i]/mid;
			}
			if(sumNode >= k){ //可以切割 
				l = mid+1;
				if(maxlen < mid){
					maxlen = mid;
				}
			}else{
				r = mid;
			}
		}
		cout<<maxlen<<endl;
	}
	return 0;
}

问题4: 排序

#include<algorithm>
bool cmp();
sort(首地址,尾地址,cmp);
stable_sort(首地址,尾地址,cmp);//值相同的情况下保证是稳定排序

//结构体类型的排序
vector<结构体> vt;
sort(vt.begin(),vt.end(),cmp);

//普通类型排序
int/char/double a[MAX];
sort(a,a+MAX,cmp);

//堆排序
//结构体类型的排序
struct cmp
{
    bool operator() (结构体 j1,结构体 j2)
    {return j1.num>j2.num?j1.num:j2.num;}
};


priority_queue<结构体,vector<结构体>, cmp >;
//普通类型的堆排序
priority_queue<int/char/double, vector<int/char/double>, greater<int/char/double> >;
priority_queue<int/char/double, vector<int/char/double>, less<int/char/double> >;

排序原理:链接:八大排序算法详解

7. 其他数学问题

问题1: 快速幂和矩阵快速幂

快速幂模板

#include<bits/stdc++.h>
using namespace std;

typedef long long ll;

ll quick_pow(int a,int m){
	ll ans = 1;
	while(m){
		if(m&1) ans *= a;
		m>>=1; 
		a*=a;
	}
	return ans;
}

int main(){
	cout<<quick_pow(2,10);
	return 0;
} 

矩阵快速幂模板

//矩阵快速幂
#include<bits/stdc++.h>
#define N 2
using namespace std;
//求矩阵相乘,放于ans
int cal(int a[N][N],int ans[N][N]){
	int temp[N][N];
	for(int i = 0;i<N;i++){
		for(int j = 0;j<N;j++){
			temp[i][j] = 0;
			for(int k = 0;k<N;k++){
				temp[i][j] += a[i][k] * ans[k][j];
			}
		}
	}
	memcpy(ans,temp,sizeof(int)*N*N);
}

int main(){
	
	//a的n次方
	int n = 3;
	int a[N][N] = {1,0,0,1};
	int ans[N][N] = {1,0,0,1};
    //矩阵快速幂
	while(n!=0){
		if(n&1){
			cal(a,ans);
		}
		n>>=1;
		cal(a,a);
	}
	for(int i = 0;i<2;i++) {
		for(int j = 0;j<2;j++){
			cout<<ans[i][j]<<" ";
		}
		cout<<endl;
	}	

	return 0;
}

矩阵快速幂典型例题:

题目:垒筛子

    垒骰子
    
    赌圣atm晚年迷恋上了垒骰子,就是把骰子一个垒在另一个上边,不能歪歪扭扭,要垒成方柱体。
    经过长期观察,atm 发现了稳定骰子的奥秘:有些数字的面贴着会互相排斥!
    我们先来规范一下骰子:1 的对面是 4,2 的对面是 5,3 的对面是 6。
    假设有 m 组互斥现象,每组中的那两个数字的面紧贴在一起,骰子就不能稳定的垒起来。 
    atm想计算一下有多少种不同的可能的垒骰子方式。
    两种垒骰子方式相同,当且仅当这两种方式中对应高度的骰子的对应数字的朝向都相同。
    由于方案数可能过多,请输出模 10^9 + 7 的结果。
    
    不要小看了 atm 的骰子数量哦~
    
    「输入格式」
    第一行两个整数 n m
    n表示骰子数目
    接下来 m 行,每行两个整数 a b ,表示 a 和 b 数字不能紧贴在一起。
    
    「输出格式」
    一行一个数,表示答案模 10^9 + 7 的结果。
    
    「样例输入」1;
    2 1
    1 2
    
    「样例输出」
    544
    
    「数据范围」
    对于 30% 的数据:n <= 5
    对于 60% 的数据:n <= 100
    对于 100% 的数据:0 < n <= 10^9, m <= 36
    
    
    资源约定:
    峰值内存消耗 < 256M
    CPU消耗  < 2000ms
    
    
    请严格按要求输出,不要画蛇添足地打印类似:“请您输入...” 的多余内容。
    
    所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
    
    注意: main函数需要返回0
    注意: 只使用ANSI C/ANSI C++ 标准,不要调用依赖于编译环境或操作系统的特殊函数。
    注意: 所有依赖的函数必须明确地在源文件中 #include <xxx>, 不能通过工程设置而省略常用头文件。
    
    提交时,注意选择所期望的编译器类型。

求解

求解

    #include<bits/stdc++.h>
    #define mod 1000000007
    #define N 7
    using namespace std;
    
    long long rule[7][7];
    void cal(long long  a[N][N],long long ans[N][N]){
    	long long temp[N][N];
    	for(int i = 1;i<N;i++){
    		for(int j = 1;j<N;j++){
    			temp[i][j] = 0;
    			for(int k = 1;k<N;k++){
    				temp[i][j] = (temp[i][j] % mod + ((a[i][k] %mod * ans[k][j]%mod) %mod) %mod)%mod;
    			}
    		}
    	}
    	memcpy(ans,temp,sizeof(int)*N*N);
    }
    
    long long power(long long a,long long b){
    	long long ans = 1;
    	a = a %mod;
    	while(b) {
    		if(b&1){
    			ans = (ans%mod * a%mod)%mod;
    		}
    		b>>=1;
    		a = (a%mod*a%mod)%mod;
    	}
    	return ans;
    }
    int main(){
    	int n,m;
    	cin>>n>>m;
    	int a,b;
    	for(int i = 1;i<=6;i++){
    		fill(rule[i]+1,rule[i]+7,1);
    	}
    //	for(int i = 1; i <=6;i++){
    //		for(int j = 1;j<=6;j++){
    //			cout<<rule[i][j]<<" ";
    //		}
    //		cout<<endl;
    //	}
    	for(int i = 0;i<m;i++){
    		cin>>a>>b;
    		rule[a][b] = rule[b][a] = 0;
    	}
    	long long nn = n-1;
    	long long anss[N][N];
    	for(int i = 1;i<N;i++){
    		for(int j = 1;j<N;j++){
    			anss[i][j] = 0;
    			if(i == j) anss[i][j] = 1;
    		}
    	}
    	while(nn){
    		if(nn&1){
    			cal(rule,anss);
    		}
    		nn>>=1;
    		cal(rule,rule);
    	}
    	int ans = 0;
    	for(int i =1;i<=6;i++){
    		for(int j = 1; j<=6;j++){
    			ans = (ans % mod + anss[i][j] %mod) %mod;
    		}
    	}
    	cout<<(ans*power(4,n)) %mod <<endl;
    	return 0;
    }

问题2: 前缀和、区间和

[占坑,后补]

posted @ 2019-03-12 17:24  pigcv  阅读(747)  评论(0编辑  收藏  举报