2021 9.14 p.m.小结 以及 数独问题探索(T3)

T1:问题 A: 组合的输出

题目描述

排列与组合是常用的数学方法,其中组合就是从 n 个元素中抽出 r 个元素(不分顺序且 r<=n),我们可以简单地将 n 个元素理解为自然数 1,2,…,n,从中任取 r 个数。
现要求你用递归的方法输出所有组合。
例如 n=5,r=3,所有组合为:
l 2 3 l 2 4 1 2 5 l 3 4 l 3 5 1 4 5 2 3 4 2 3 5 2 4 5 3 4 5

输入

一行两个自然数 n、r(1<n<21,1<=r<=n)。

输出

所有的组合,每一个组合占一行且其中的元素按由小到大的顺序排列,每个元素占三个字符的位置,所有的组合也按字典顺序。

样例输入

5 3

样例输出

  1  2  3

  1  2  4

  1  2  5

  1  3  4

  1  3  5

  1  4  5

  2  3  4

  2  3  5

  2  4  5

  3  4  5

题解

这道题考察了排列数的问题。只需要用一个pd数组来枚举每一层(每一位)选什么数,再用一个VIS数组来记录是否选过,若没选则选,否则下一个。注意可能用快读快写优化时间。

参考代码

#include<cstdio>
using namespace std;
int n,r,x[100],vis[100];
void dfs(int k)
{
	if(k==r+1)
	{
		for(int i=1;i<=r;i++)
		printf("%3d",x[i]);
		printf("\n");
		return ;
	}
	for(int i=x[k-1]+1;i<=n;i++)
	{
		if(!vis[i])
		{
			vis[i]=1;
			x[k]=i;
			dfs(k+1);
			vis[i]=0;
		}
	}
}
int main()
{
	scanf("%d%d",&n,&r);
	dfs(1);
	return 0;
}

T2:问题 B: 门票问题

题目描述

    有很多人在门口排队,每个人将会被发到一个有效的通行密码作为门票。一个有效的密码由L(3≤L≤15)个小写英文字母组成,至少有一个元音(a、e、i、o或u)和两个辅音(除去元音以为的音节),并且是按字母表顺序出现的(例如abc是有效的,而bac不是)。
    现在给定一个期望长度为L和C(1≤C≤26)个小写字母,写一个程序,输出所有的长度为L、能由所给顶的C个字母组成的有效密码。密码必须按字母表顺序打印出来,一行一个。

输入

    第1行是两个由一个空格分开的正整数,L和C,3≤L≤15,1≤C≤26。
    第2行是C个由一个空格隔开的小写字母密码是由这个字母集中的字母来构建的。

输出

    若干行,每行输出一个长度为L个字符的密码(没有空格)。输出行必须按照字母顺序排列。程序只需要输出前25000个有效密码,即使后面还存在有效密码。

样例输入

4 6

a t c i s w

样例输出

acis

acit

aciw

acst

acsw

actw

aist

aisw

aitw

astw

cist

cisw

citw

istw

题解

本题与上一道题相比,多了一些要求:至少一个元音,2个辅音。因此我们可以在搜索的过程中来剪枝。首先可以维护一个前缀和(倒着的),表示后面元音的个数。搜索过程中,如果当前枚举到了k,前面没有元音,而后面也没有元音了(前缀和),那么就可以直接return。同理,辅音的处理与这个类似,只不过是个数<2。同时如果接下来的字母都用上,还达不到既定长度,那么也return。有了3重剪枝,就能极大的优化时间效率。

参考代码

#include<cstdio>
#include<algorithm>
using namespace std;
int l,c,x[100],seg[500],p[100],vis[100],tot=0;
char s[100];int pd1[500],pd2[500],sis=0,ht[500];
bool pd(int k) { return k=='a'||k=='e'||k=='i'||k=='o'||k=='u'; }
void dfs(int k)
{
	if(tot>25000) return;
	pd1[k]=pd1[k-1];pd2[k]=pd2[k-1];
	if(k==l+1)
	{
		if(pd1[k]==0||pd2[k]<2) return;
		tot++;
		if(tot>25000) return;
		for(int i=1;i<=l;i++)
		putchar(p[i]);
		printf("\n");
		return;
	}
	for(int i=seg[p[k-1]]+1;i<=c;i++)
	{
		if(tot>25000) return;
		if(vis[i]) continue;
		if(i==sis&&pd1[k]==0) break;
		if(pd2[k]+ht[i]<2) break;
		if(c-i+1+k<l) break;
		vis[i]=1;p[k]=x[i];
		if(pd(x[i])) 
		{
			pd1[k]++;
			dfs(k+1);
			pd1[k]--;
		}
		else
		{
			pd2[k]++;
			dfs(k+1);
			pd2[k]--;
		}
		vis[i]=0;
	}
}
int main()
{
	scanf("%d%d",&l,&c);
	for(int i=1;i<=c;i++)
	{
		scanf("%s",s);
		x[i]=s[0];
	 } 
	sort(x+1,x+c+1);
	for(int i=c;i>=1;i--) 
	{
		seg[x[i]]=i;ht[i]=ht[i+1];
		if(pd(x[i])&&sis==0)
		sis=i+1;
		if(!pd(x[i]))
		ht[i]++;
	}
	dfs(1);
	return 0;
}

T3:问题 C: 子集和问题

题目描述

子集和问题的一个实例为〈S,c〉。其中,S={ x1, x2,…, xn}是一个正整数的集合,c是一个正整数。子集和问题判定是否存在S的一个子集S1,使得子集S1和等于c。
对于给定的正整数的集合S={ x1, x2,…, xn}和正整数c,编程计算S 的一个子集S1,使得子集S1和等于c。

输入

第1行有2个正整数n和c,n表示S的个数,c是子集和的目标值。

接下来的1 行中,有n个正整数,表示集合S中的元素。

n<=8000 c<=10^7

输出

子集,各元素之间用空格隔开。当问题无解时,输出“No solution!”。

样例输入

5 10

2 2 6 5 4

样例输出

2 2 6

题解

这道题忽略数据范围吧。现在只考虑怎么完成这道题。同样的,用一个pd数组维护sum,如果sum=c就全部return。当然,还要用x数组记录所选的元素。这样还不够,需要一点小剪枝。如果此刻的sum大于c,return;如果后面的数的和加上sum都小于c,那么也没必要继续搜了,直接return。如此就能找到一组答案就退出,或者输出问题无解。

参考代码

#include<cstdio>
#define LL long long
using namespace std;
int n,c,a[10000],sum=0,sum1[10000];
int pd=0,x[10000];
void dfs(int k)
{
	if(sum+sum1[n]-sum1[x[k-1]]<c) return;
	if(pd==1) return;
	if(sum==c)
	{
		for(int i=1;i<k;i++) printf("%d ",a[x[i]]);
		pd=1;return;
	}
	for(int i=x[k-1]+1;i<=n;i++)
	{
		x[k]=i;
		if(sum+a[i]>c) continue;
		sum+=a[i];
		dfs(k+1);
		sum-=a[i];
	}
}
int main()
{
	scanf("%d%d",&n,&c);
	for(LL i=1;i<=n;i++) 
	{
		scanf("%d",&a[i]);
		sum1[i]=sum1[i-1]+a[i];
	}
	 dfs(1);
	if(pd==0) printf("No Solution!");
	return 0;
}

T5:问题 E: 数独

题目描述

数独是一种传统益智游戏,你需要把一个9 × 9的数独补充完整,使得图中每行、每列、每个3 × 3的九宫格内数字1~9均恰好出现一次。

请编写一个程序填写数独。

 

输入

输入包含多组测试用例。

每个测试用例占一行,包含81个字符,代表数独的81个格内数据(顺序总体由上到下,同行由左到右)。

每个字符都是一个数字(1-9)或一个”.”(表示尚未填充)。

您可以假设输入中的每个谜题都只有一个解决方案。

文件结尾处为包含单词“end”的单行,表示输入结束。

输出

每个测试用例,输出一行数据,代表填充完全后的数独。

样例输入

.2738..1..1...6735.......293.5692.8...........6.1745.364.......9518...7..8..6534.

......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.

样例输出

527389416819426735436751829375692184194538267268174593643217958951843672782965341

416837529982465371735129468571298643293746185864351297647913852359682714128574936

题解

        对于这道题我也算是颇有心得。我的代码对于单个的九宫格不输任何表答,但就是要跑2s!!所有我这个代码无法过数独这道题(泪流满面,蒟蒻的悲伤~)。现在来谈谈这道题。首先用一个e_map存一下地图,然后把每一行、每一列、每一个3*3方块压缩成二进制保存下来。再把每一行还没有填的格子记录一下(这样就不用重复找已经填过的数),然后就搜呗。

        搜的话很简单了,只用在(横着的没填的、竖着的没填的、方块里没填的)求交集,然后枚举数字即可。注意,是!没!填!的!因此我们直接把之前二进制压缩的结果亦或(^)511(二进制111111111)就可以了,相当于全部取个反(现实意义)。处理了这一位的数字后,来考虑下一次枚举那一个位置。这时我就直接用了一个!!!大优化!!!就是每次把每一行能取的位置倒着枚举,然后……直接取,并且把当前行的总共能填的总数-1,并且在回溯之后加回去就可以了。这一步十分精妙,可以动态剪枝,并且由于我的每一行总数组不变,变的只是尾指针,因此答案也是正确的,这部分参见代码中search函数。

         为了进一步优化,我还用冒泡排序(个人认为应该用快排,可能会快一些,但是……写不来那种)把每一行的能取的位置排了个序,关键字就是这个位置能填的数的可能性,可能性越小,说明需要回溯的机会越少,也就能越快的找到答案。还有一个明显的优化,就是找到了唯一的一组解后,直接return,并且要让所有正在运行的函数全部return,因此我定义了ed,当ed=1时,表示答案已找到,此时我在大量的区域做了ed的判断(其实就2处),加快return的速度。对于找数字,此处还运用了lowbit的知识来优化选择,有兴趣者可以自行查阅,不用lowbit也能跑,for就行了,但是时间效率就会损失的比较多。

        还有一点,用tot存一下此刻还有多少空格没有选,当tot=0时表示答案已经出现,此刻更新ed,并且输出答案。特别注意各个函数的初始化,这点十分重要。至于那几个输出的函数,只是为了验算一些东西,可自行理解。如何处理3*3的块?如果看过代码(…………此处省略一万字),就能发现我的数组的更新就解释了这一点。(x+2)/3能够准确表示这是上中下的那一块区域,同理对于y也能确定在哪一竖行。

        代码最后可以计算出单纯dfs的运行时间,我就是通过这个来判断时间的,注意最后的输出毫秒是浮点数,要用ctime的头文件。最后为了提速,还用了各种方法,只是效果皆不明显,此处就省略了。最后注意!!!这代码能很快求一个的答案,却没法过!!! 

        对了,这个代码最后有很多我自己找的数据,答案可用下面代码对拍。

参考代码

#include<cstdio>
#include<ctime>
#include<iostream>
using namespace std;
char s[2000];int e_map[10][10],tot_x[10],max0=-1,tot=0;
int tick_x[20],tick_y[20],tick_k[5][5],st_x,st_y;
int max1(int p,int q) { return p>q?p:q; }
int ed=0,lg[1025],went[10][10],goes[10];
void outputing()
{
	for(int i=1;i<=9;i++)
		for(int j=1;j<=9;j++)
			printf("%d",e_map[i][j]);
	printf("\n");
}
void output2()
{
	for(int i=1;i<=9;i++)
	{
		for(int j=1;j<=9;j++)
	    printf("%d",e_map[i][j]);
	printf("\n");	
	}
	  printf("\n");
}
void output3(int k)
{
	for(int i=1;i<=9;i++)
	{
		printf("%d",k%2);
		k/=2;
	}
	printf("\n");
}
int search()
{
	for(int i=1;i<=9;i++)
	{
		for(int k=goes[i];k>=1;k--)
		{
			int j=went[i][k];
			st_x=i;st_y=j;
			goes[i]--;
			return i;
            //找到一个直接取位置就可以
            //后面会把goes再加回去
		}
		
	}
}
void dfs(int x,int y)
{
	if(ed==1) return;
	if(tot==0)
	{
		outputing();
		ed=1;
		return;
	}
	int ttt=tick_x[x]^511;
    //取反
	while(ttt)
	{
		if(ed==1) return;
		int pk=ttt&-ttt;ttt-=pk;//lowbit取数
		if((pk&tick_y[y])) continue;
		if((pk&tick_k[(x+2)/3][(y+2)/3])) continue;
        //行、列、块不冲突
		tick_x[x]|=pk;
		tick_y[y]|=pk;
		tick_k[(x+2)/3][(y+2)/3]|=pk;
        //短暂更新地图(行列块)的数据
		e_map[x][y]=lg[pk];
		tot--;
		int rst=search();
		dfs(st_x,st_y);
        //把取出来的数“放回去”,关键的一步剪枝
		goes[rst]++;
		tot++;
		tick_x[x]^=pk;
		tick_y[y]^=pk;
		tick_k[(x+2)/3][(y+2)/3]^=pk;
        //回溯需要还原
		e_map[x][y]=0;
	}
}
int main()
{
	lg[1]=1;lg[2]=2;lg[4]=3;
	lg[8]=4;lg[16]=5;lg[32]=6;
	lg[64]=7;lg[128]=8;lg[256]=9;
    //为lowbit的计算做准备
	while(1)
	{
		tot=ed=max0=0;
		scanf("%s",s);
		if(s[0]=='e') break;
		for(int i=1;i<=3;i++)
		  for(int j=1;j<=3;j++)
		    	tick_k[i][j]=0;
        //数组初始化
		for(int i=1;i<=9;i++)
		{
			goes[i]=0;//每一行尾指针
		   	tick_x[i]=0;
		   	tick_y[i]=0;
		   	for(int j=1;j<=9;j++)
		   	 went[i][j]=0;
		}
		for(int i=1;i<=9;i++)
		  for(int j=1;j<=9;j++)
		    {
		    	if(s[(i-1)*9+j-1]=='.')
		    	{
		    		went[i][++goes[i]]=j;//记录每一行合法位置
		    		e_map[i][j]=0;
		    		tot++;
				}
		    	else 
				{
                    //二进制压缩
					e_map[i][j]=s[(i-1)*9+j-1]-'0';
					tick_x[i]|=(1<<(e_map[i][j]-1));
					tick_y[j]|=(1<<(e_map[i][j]-1));
					tick_k[(i+2)/3][(j+2)/3]|=(1<<(e_map[i][j]-1));
				}
			}
            //冒泡排序
			for(int i=1;i<=9;i++)
			  for(int j=1;j<=9;j++)
			    for(int k=1;k<=9;k++)
			      if(went[i][j]>went[i][k])
			        {
			        	int c=went[i][k];
			        	went[i][k]=went[i][j];
						went[i][j]=c;
					}
			search();//找出初始位置
//			clock_t t;
//		t=clock();
		dfs(st_x,st_y);
//		t=clock()-t;
//		cout<<"时间为:"<<(((float)t)/CLOCKS_PER_SEC*1000)<<"ms\n";	  
        //记录dfs时间
	}
	return 0;
}

.................................................................................
.2738..1..1...6735.......293.5692.8...........6.1745.364.......9518...7..8..6534.
......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
1.3...5.9..21.94.....7.4...3..5.2..6.6.....5.7..8.3..4...4.1.....92.58..8.4...1.7
8..........36......7..9.2...5...7.......457.....1...3...1....68..85...1..9....4..
..53.....8......2..7..1.5..4....53...1..7...6..32...8..6.5....9..4....3......97..
98.7..6..7......8...6.5....4....3.....89...7.....2.3...1....2....75...6......1.54
.3...7..46.2.41....5..3.967.4...3..6.87...35.9..7...2.718.2..4....16.8.94..5...3.
..5..43.1..........28..6.4....4.5...7......6.6......3...3...17....1.9.....9..2..8
.6.....75....4..9.8.3........8.9.....72..6...6..4.5...12.6......3..7.2....7..431.
...8......3..92..1.9..3....7...498....57..9..2....6.....352.1...5....4.2..6..4...
43.28...............84...1.9.3..2.4..7..3..2..5.9..8.1.2...65...............41.76
57.12...............67...8.3.4..9.7..2..7..5..1.3..9.2.8...21...............54.63
...6....7...849...28......5..7315....4..7......2......91..8..6.7......9...34..5..
75..9..469.1...3.2.........2..6.1..7.8.....2.1..3.8..5.........3.9...2.484..3..79
end

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值