跟着左神学算法——数据结构与算法学习日记(九)

285 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情

N皇后问题

N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列,也不在同一条斜线上 给定一个正数n,返回n皇后的摆法有多少种 n=1,返回1 n=2 或 n=3,2皇后和3皇后无论怎么摆都不行,返回0 n=8,返回92

代码实现:

//主函数
public static int num1(int n){
	if(n<1) return 0;

	int[] record = new int[n];//record[i]->第i行的皇后放在了第record[i]列
	return process1(0,record,n);
}
//递归函数process1   i:现在要摆第i行的皇后  n:问题规模,n个皇后,棋盘n*n
//record:已经摆好了的皇后       返回值:在当前record的基础上,第i行到第i-1行的皇后摆法数量
public static int process1(int i,int[] record,int n){
	if(i == n) {
		//递归走完了,说明当前情况是符合条件的一种
		return 1;
	}

	int res = 0;
	for(int j = 0;j<n;j++){
		//当前第i行的皇后,放在j列上是否会与record中已经摆好的皇后产生冲突
		//如果是,认为无效;如果不是,认为有效,把第i行的皇后填到第j列
		if(isValid(record,i,j)){
			record[i] = j;
			res+=process(i+1,record,n);
		}
	}
	return res;
}
//判断是否产生冲突的函数isValid
public static boolean isValid(int[] record, int i, int j){
	for(int k = 0;k<i;k++){
		if(j == record[k] || Math.abs(record[k]-j) == Math.abs(i-k)){
			return false;
		}
	}
	return true;
}

N皇后问题优化

思路:采用二进制中位运算的特殊性,使用一个整形变量limit来代表当前可以填入皇后且不会产生冲突的位置。比如4皇后问题:一开始limit为000..01111(最后四位为1,代表当前可以填入皇后且不会产生冲突的位置有4个),8皇后问题limit则为000...011111111(都是总共32位)。

有几个皇后就从最后开始数几位变成1

以8皇后问题为例:一开始limit为000..011111111,当填入第一行的一个皇后时,limit变化为000...011011111(填入皇后后对应的位置变为0),那么第二行不能填的位置由三种限制产生:第一种是同一列的位置不能填,因此limit1为000...011011111(1为可以填入的位置,0为不可以);第二种限制是左斜线不能填,因此limit2为000...010111111;第三种限制是右斜线不能填,因此limit3为000...011101111。将这三个limit进行位运算上的或运算:limit1 | limit2 |limit3 = 000...010001111(1为可以填入的位置,0为不可以),这样子,我们就知道下一行的皇后不能填的位置就为第1、2、3列(索引从0开始)。

注意:这种优化并没有对N皇后问题的时间复杂度进行优化,时间复杂度仍为O(N^N),位运算只是将N皇后问题的速度提高了一个比较大的常数等级

代码实现:

//获取n皇后结果的主函数
public static int num2(int n){
	if(n<1 || n>32){//整形变量32位,n超过32位int类型不能处理
		return 0;
	}
	int limit = n == 32 ? -1 : (1<<n) - 1;//-1的二进制表示为111...111(32个1)、
	return process2(limit,0,0,0);
}

//递归函数process2
// colLim 列的限制,1的位置不能放皇后,0的位置可以
// leftDiaLim 左斜线的限制,1的位置不能放皇后,0的位置可以
// rightDiaLim 右斜线的限制,1的位置不能放皇后,0的位置可以
public static int process2(int limit,int colLim,int leftDiaLim,int rightDiaLim){
	if(colLim == limit){
		return 1;
	}
	
	int mostRightOne = 0;
	//pos是候选皇后可以选的所有位置
	int pos = limit & (~(colLim | leftDiaLim | rightDiaLim));//pos中0表示不能放皇后的位置,而1表示能放皇后的位置
	int res = 0;

	while(pos != 0){//pos等于0的话说明没有位置可以选了
		mostRightOne = pos & (~pos + 1);//提取出pos中最右侧的1
		pos = pos - mostRightOne;//将皇后放在最右侧的1位置处,更新pos
		res += process2(limit, colLim | mostRightOne, (leftDiaLim | mostRightOne)<<1,(rightDiaLim | mostRightOne) >>1 );
	}

	return res;
}

会议安排问题

题目:一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。给你每一个项目开始的时间和结束的时间,试安排宣讲日程,要求会议室进行的宣讲的场次最多。返回这个最多的宣讲场次。

思路:按照结束时间的早晚进行安排,结束时间最早的安排在最前面。

实现代码:

//会议定义
public static class Program{
	public int start;//开始时间
	public int end;//结束时间

	public Program(int start, int end){
		this.start = start;
		this.end = end;
	}
}

public static class ProgramComparator implements Comparator<Program>{
	
	@Override
	public int compare(Program o1,Program o2){
		return o1.end - o2.end;
	}
	
}

public static int bestArrange(Program[] programs, int timePoint){//timePoint:上一个会议的结束时间
	Array.sort(programs,new ProgramComparator());
	int result = 0;
	for(int i = 0;i<programs.length;i++){
		if(timePoint <= programs[i].start){
			result++;
			timePoint = programs[i].end;
		}
	}
	return result;
}

金条切割问题

题目:一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金条,不管切成长度多大的两半,都要花费20个铜板。 一群人想整分整块金条,怎么分最省铜板? 例如,给定数组{10,20,30},代表一共三个人,整块金条长度为10+20+30=60.金条要分成10,20,30三个部分。如果先把长度60的尽快分成10和50,花费60;再把长度50的金条分成20和30,花费50;一共花费110铜板; 但是,如果先把长度60的金条分成30和30,花费60;再把长度30的金条分成10和20,花费30,一共花费90铜板。 输入一个数组,返回分隔的最小代价。

思路:从小的开始,先把数组中最小的两个数值a和b相加得到c,数组删去a和b,然后c和数组中最小的数值d相加得e,数组删去d,然后e和数组中最小的元素f相加....直到数组中没有元素时,累加的结果就是最小代价。 所以我们可以维护一个小根堆,每次取堆顶元素即可获得数组中的最小元素

代码实现:

public static int lessMoney(int[] arr){
	PriorityQueue<Integer> pQ = new PriorityQueue<>();
	for(int i = 0;i<arr.length;i++){
		pQ.add(arr[i]);
	}

	int sum = 0;
	int cur = 0;
	while(pQ.size()>1){
		cur = pQ.poll();
		sum += cur;
		pQ.add(cur);
	}
	return sum;
}