人生不易,生活无趣。继续继续。
区间k大数查询:
问题描述
给定一个序列,每次询问序列中第l个数到第r个数中第K大的数是哪个。
输入格式
第一行包含一个数n,表示序列长度。
第二行包含n个正整数,表示给定的序列。
第三个包含一个正整数m,表示询问个数。
接下来m行,每行三个数l,r,K,表示询问序列从左往右第l个数到第r个数中,从大往小第K大的数是哪个。序列元素从1开始标号。
输出格式
总共输出m行,每行一个数,表示询问的答案。
样例输入
5
1 2 3 4 5
2
1 5 2
2 3 2
样例输出
4
2
数据规模与约定
对于30%的数据,n,m<=100;
对于100%的数据,n,m<=1000;
保证k<=(r-l+1),序列中的数<=106。
首先看到最后数据规模,还行,不大,不需要特殊考虑。
一点点来吧:
1、读入n,数组的大小
2、读入n个数,组成数组。(how?)
3、读入m查询条数
4、循环m次,每次读入l,r,k。从数组l(left)到r(right)之间,第k大的数字,找出来,任务完成。
第一步略过,我们看第二步,读入一个数组,直接点的思维:
已经有了大小n,循环读出n个数字,一个个放到数组中去。可以,没问题。大家有简单方法下面评论,我这里写一种:
String arr[] = sc.nextLine().toString().split(" ");
int array[] = new int[arr.length];
for(int i = 0; i < arr.length; i++){
array[i] = Integer.parseInt(arr[i]);
}
当然没高级到哪里去,就是整行当string读入,按‘ ’split,再一个个转换成int。可能回比前面的方法慢点,不讨论复杂度了,欢迎大家在下面发言吧。
如果换做python,我会这样写:
arr = list(map(int,input().split()))
一样的思维,有玩python的,喜欢就看下吧。大概就是这种感觉:
我们继续下一步,m次循环,每次循环做的处理都是一样的,考虑下分装(不是硬性要求,思维不乱就行),写个处理方法。
我们先考虑方法的参数需要什么:
1、待查找的数组。(需要根据left,right和array来产生)
2、k的值。
方法返回的就是待查找数组第k大的数字。
第一步,待查找数组怎么生成?用left和right算出数组长度,再从array里面根据下标一个个拿出来,产生一个新的数组。可以,没问题。这里介绍一个数组复制的方法(有很多):
Arrays.copyOfRange(array, from, to),参数已经不言而喻了,array是要复制的数组,from和to可以根据left和right确定,需要注意一点。区间是前闭后开,即:[from,to)。copy的时候注意下标就好。
得,数组和k都拿到了,然后处理,找打第k大的数组。不用考虑了,数组排序,前面我们说到了一个Array.sort()。(原地排序)然后根据k下标返回就可以了,注意点,题意从大到小的第k大,sort方法是升序排序。也就是说逻辑上上要从后往前找k个,具体实现上,下标有array.lenth和k共同确定就好了。
致此思路全部写完,看代码:
import java.util.*;
public class Main{
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = Integer.parseInt(sc.nextLine());
String arr[] = sc.nextLine().toString().split(" ");
int array[] = new int[arr.length];
for(int i = 0; i < arr.length; i++){
array[i] = Integer.parseInt(arr[i]);
}
int m = sc.nextInt();
int l,r,k;
for(int i = 0;i<m;i++){
l = sc.nextInt();
r = sc.nextInt();
k = sc.nextInt();
System.out.println(find(k,Arrays.copyOfRange(array, l-1, r)));
}
sc.close();
}
private static int find(int k, int[] arr) {
// TODO Auto-generated method stub
Arrays.sort(arr);
return arr[arr.length - k];
}
}
还有一点注意,题目数组标号从1开始,注意下标。
二、最大最小公倍数:
问题描述
已知一个正整数N,问从1~N中任选出三个数,他们的最小公倍数最大可以为多少。
输入格式
输入一个正整数N。
输出格式
输出一个整数,表示你找到的最小公倍数。
样例输入
9
样例输出
504
数据规模与约定
1 <= N <= 10^6。
数据规模不大,但还是要考虑,因为最后结果是三个数的最小公倍数。那结果的可能性就已经出了Integer的最大表示范围。
数据类型我们考虑完了,来分析一下这个题。找任意三个数的最小公倍数,然后挑出那个最大的值。(自然我们就要从大数考虑)
官网上锦囊是“贪心算法”,嗯,我贪心了,被判处了“终身超时”,没收了全部分数。(或许c可以,试试吧。)
贪心好啊,一堆循环,无脑跑就好了。但是少考虑了很多东西:
首先a,b,c三个数的最小公倍数的最大值是什么?a*b*c!毫无疑问。但是是有前提的,三个数必须互质。(这里有条结论,我不知是对是错,有数学大佬屈尊在下面简单分析下这倒题。大于一的两个连续的数必定互质。)
这里有篇文章,讲的挺好的,如果不想换网页了,就看我下面胡扯吧。
https://blog.csdn.net/cugsl/article/details/79614907
现在考虑这样的问题n,n-1,n-2。三个连续的数字,什么时候他们的最小公倍数是n * n-1 * n-2? n为奇数时!
n为奇数,那么这三个数是奇偶奇。没有公因子2!假设n % 3 = 0,那么前一个能整除三的是n-3。因此这三个数没有公因子3,同理,公因子4,5....都没有。证毕。(结论,n,n-1,n-2。如果n为奇数,最小公倍数是n * n-1 * n-2)
来考虑下一种,n为偶数。那就是偶奇偶的组合。1和3就会有公因子2。很容易的想到,我们可以将三位数全部后移,n为偶数,那么n-1,n-2,n-3,就是我们的第一种情况了。但这不是最大的,因为还要这种:n,n-1,n-3。他的结构是偶奇奇,而且显然比n-1 * n-2 * n-3要大,但是!有前提,n不能被3整除,不然n和n-3就有公因子3。
分析致此完毕了。
if n % 2 != 0:
result = n * n-1 * n-2
else:
if n % 3 != 0:
result = n * n-1 * n-3
else:
result = n-1 * n-2 * n-3
这就是逻辑思路,你向往的贪心循环,被三个判断抽了脸。无论从空间复杂度还是时间复杂度上,你都看不到他的车尾灯。
(我记得有个舍友说:你编程境界提高的表现就在于人想得少,机器做的多 到 人想的多,机器做的少的转变。)
这句话留给大家自己去评判。我觉得......舍友说的对啊。
看代码吧。虽然已经没有必要了:
import java.util.*;
public class Main{
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Long n = (long) sc.nextInt();
Long res;
if(n % 2 != 0)
res = n*(n-1)*(n-2);
else{
if(n%3 != 0)
res = n * (n-1) * (n-3);
else
res = (n-1)*(n-2)*(n-3);
}
sc.close();
System.out.println(res);
}
}
三、k好数
问题描述
如果一个自然数N的K进制表示中任意的相邻的两位都不是相邻的数字,那么我们就说这个数是K好数。求L位K进制数中K好数的数目。例如K = 4,L = 2的时候,所有K好数为11、13、20、22、30、31、33 共7个。由于这个数目很大,请你输出它对1000000007取模后的值。
输入格式
输入包含两个正整数,K和L。
输出格式
输出一个整数,表示答案对1000000007取模后的值。
样例输入
4 2
样例输出
7
数据规模与约定
对于30%的数据,KL <= 106;
对于50%的数据,K <= 16, L <= 10;
对于100%的数据,1 <= K,L <= 100。
读完问题,想到种模型:有L把椅子,K种不同的物体,每把椅子摆放不同的种类的物体,要求某些种类物体不允许相邻,问总共有多少种摆法。
嗯,差不多就是上面的题了。我们首先想到了什么?初中或高中阶段的排列组合。
k进制,其实我们有k个数字可选0,1,2........k-1。但是选择类型其实就两种,1 是开头或结尾,2 不是开头或结尾。即选0或者k-1,还有就是1 ~ k-2两种类型。如果是第一种,那么下一位数字我们能选择的数字数是k-1(只减去与其相邻的一个数字),如果是第二种,就是k-2(这时相邻的数字就有两个)。
也就是说你下一个能选择的数字的个数,由它前面那个数字可以确定,很好想到的一种算法——递归。
但是这里不用递归,为什么?当数据量大的时候,递归的速度会以“肉眼可见”的速度减慢,甚至到不能忍受的程度。我们举个简单的例子——fibonacci数列:
1,1,2,3,5,8,13,21......
数列满足这样的运算式:f(k) = f(k-1) + f(k-2)
我们很自然的选择递归运算。但是当k的值变大的时候,运算就变得麻烦起来。举个栗子:
f(6) = f(5) + f(4)
(五毛钱特效图,麻烦钛合金眼镜拿出来~~~~)
不要关注画面效果了,我们来说递归的弊端在哪里。计算f(6),我们不严谨的说需要两步,第一步递归调用f(5),求其值。第二步,递归调用f(4),求其值。
问题在这里,f(4)的整个递归调用过程其实就是f(5)里面蓝色框框出来的那一部分,其实是计算过得,因此这个问题的递归慢的原因就是计算了没必要计算的f(4)。试想k不是6,是6000.是60000。那多余的计算会有多少!
还记得我们前面是怎么做的吗?不用递归,先做一小步,在根据结果退出下一步,直到推出我们想要的f(k)。例如f(3) = 1 + 1 = 2。然后将记录保存,f(4)直接利用f(3)的结果,不再对f(3)进行运算。这样就没得多于的运算了。
其实我认为这里就可以理解成DP(dynamic programming动态规划),我们计算f(k),不是直接计算出f(k),而是先计算出f(3),然后f(4)一点点的逼近我们所需要的结果,最终达到目的。
这个就算是DP的大白话理解吧。
返回来我们看这道题,我们一点点考虑,先考虑L为1的时候,然后再算出L为2......直到我们计算出需要的L长度的情况。
原文章在这里(支持原创):
https://www.cnblogs.com/liuzhen1995/p/6551039.html
我把我的写下面,其实是一样的
import java.util.*;
public class Main{
public static void main(String[] args) {
int mod = 1000000007;
int [][]dp = new int[101][101];
int result = 0;
Scanner sc = new Scanner(System.in);
int K = sc.nextInt();
int L = sc.nextInt();
sc.close();
for(int i = 0; i<K; i++){//L = 1情况 FIRST
dp[1][i] = 1;
}
for(int i = 2; i<=L; i++){ SECOND
for(int j = 0; j<K; j++){
for(int f = 0; f<K; f++){
if(f+1 != j && f-1 != j){
dp[i][j] += dp[i-1][f];
dp[i][j] %= mod;
}
}
}
}
for(int i = 1; i<=K; i++){ THIRD
result += dp[L][i];
result %= mod;
}
System.out.println(result);
}
}
首先看这里:int [][]dp = new int[101][101]。
数组的解释,原作者是这样解释的:
dp[ i ] [ j ]是k好数第i位数字是j的情况,而dp[ i ] [ j ]的值就是这种情况的可能个数。
之后分三步:
first:初始化dp[ 1 ][ ],即第一行的dp值(数组中是第二行)。前面解释到,dp【1】【j】,就是长度L为1时此位上的数字为j的可能个数。如果L为1,其实不用考虑嘛,全部都是1。
举个例子,L=1的四进制好数,有0,1,2,3四种:
对应数组的dp[1][0] = 1,dp[1][1] = 1,dp[1][2] = 1,dp[1][3] = 1。
因此代码中FIRST段就是在初始化L为1的情况。
来看下dp【2】【0】怎么计算,L为2且末位为0的可能个数。就可以由dp【1】【】求得,其实就是dp【1】【0】到dp【1】【k-1】除掉dp【1】【1】其他值得求和。为什么除掉dp[1][1]呢?因为1和0相邻嘛。这种情况对应的就是10,显然10不是k好数。
像这样:
【2】【0】是由【1】【0】,【1】【2】,【1】【3】求和得到,【2】【1】是由【1】【1】,【1】【3】求和得到,依次类推。
代码段里的SECOND,就是一层层的求出指定L长度并以j结尾的k好数的个数。
在执行完毕后,想想我们的第L行(数组中的L+1行)是什么东西?长度为L的的数字并且末位为0,1,2,3,......k-1的个数。那么将第L行的结果值相加就是K进制L长度k好数的个数了。不过注意一点,我们忽略了一种情况,初始位为0的情况。类似024,000这种,都是不满足k好数的定义的。因此THIRD步骤减去了初始位为零的情况。这里有个小弯:
我们没有计算dp【L】【0】的值,而是此行后面的数字求和。但是我们知道,dp【L】【0】是k好数第L位上为0的个数。我们不是应该减去初始位为零的个数嘛?
试想这个问题:
一个数是k好数:1357,那么数字的“倒置”——7531一定是个k好数。以0开头的数字,反过来后就是以0结尾,而且如果反过来后首位不为零,也就是说原数字末尾部位0,拿他也是一个k好数。什么意思?就是说我们考虑的所有情况里面,只要有一个0开头的数字,就有一个0结尾的数字(k好数翻转还是k好数)。再简化,0开头数字和0结尾数字一样多。GOT?
这个问题就到此结束了,循环中还用了个小技巧,每次执行完后都mod了一下,避免数据大后,数据超出int范围造成数据丢失,最终导致结果错误。
四、节点选择
问题描述
有一棵 n 个节点的树,树上每个节点都有一个正整数权值。如果一个点被选择了,那么在树上和它相邻的点都不能被选择。求选出的点的权值和最大是多少?
输入格式
第一行包含一个整数 n 。
接下来的一行包含 n 个正整数,第 i 个正整数代表点 i 的权值。
接下来一共 n-1 行,每行描述树上的一条边。
输出格式
输出一个整数,代表选出的点的权值和的最大值。
样例输入
5
1 2 3 4 5
1 2
1 3
2 4
2 5
样例输出
12
样例说明
选择3、4、5号点,权值和为 3+4+5 = 12 。
数据规模与约定
对于20%的数据, n <= 20。
对于50%的数据, n <= 1000。
对于100%的数据, n <= 100000。
权值均为不超过1000的正整数。
树形动态规划,大家可能有很多很好的办法。我找到了篇理解上比较简单的文章,原文章链接在这里:
https://www.cnblogs.com/liuzhen1995/p/6550442.html
动态规划问题,一般先找到状态的变化情况,这之后,我们就会再次卡在代码实现上......
哈,开玩笑,我们分析下这个题。
我们用个数组dp,dp【i】【0】表示不选择节点i时,在以此节点为根节点的树上能拿到的最大权值。dp【i】【1】是选择节点i是我们能拿到的最大权值。
状态变化处理如下:
对于叶子节点:我们选择获得的权值就是叶子节点的权值,不选就是0。也就是dp【i】【0】=0,dp【i】【1】=i节点的权值。
非叶子节点:我们选择能获得的权值是此节点权值加上他所有孩子节点不选择能获得的权值(代码中递归实现)。不选的的话,就是所有孩子节点能获得最大权值和。即:
我们最终的结果就是:
根节点选与不选中最大的值。
思路就是这么个思路,很好理解,代码做起来复杂度挺高的。这个代码并没有把所有的例子测试通过,有几组超时了:
import java.util.Scanner;
public class Main {
public int[][] dp = new int[100002][2];
public int[][] tree = new int[100002][300]; //tree[i][3] = num表示第i个节点的第3个孩子节点为第num个节点
/*
* 参数point1:表示输入的第point1个节点,不是节点权值
* 参数point2:表示输入的第point2的节点,不是节点权值
* 说明:由于题目仅仅给出边的说明,并未说明两个节点谁是父母节点,所以以下有两种情形
*/
public void creatTree(int point1, int point2) {
int i = 0;
//当第point1个节点为父母节点时
while(tree[point1][i] != 0) i++; //如果第point1个节点已经有孩子了,再增加一个孩子
tree[point1][i] = point2;
int j = 0;
//当第point2个节点为父母节点时
while(tree[point2][j] != 0) j++;
tree[point2][j] = point1;
}
/*
* 参数satrt:开始对树进行DFS遍历的开始节点,为具体节点位置,不是节点权值
* 参数root:为第start个节点的直接父母节点位置,root = 0表示根节点的父母节点
*/
public void dfs(int start, int root) {
int child = tree[start][0]; //第start个节点的第1个孩子节点
for(int i = 0;child != 0;i++) {
child = tree[start][i];
if(child != root) { //防止出现start的孩子成为start的父亲情况
dfs(child, start);
dp[start][1] += dp[child][0]; //当第child个节点没有孩子节点时,开始回溯
dp[start][0] += (dp[child][1] > dp[child][0] ? dp[child][1] : dp[child][0]);
}
}
}
public static void main(String[] args) {
Main test = new Main();
Scanner in = new Scanner(System.in);
int n = in.nextInt();
for(int i = 0;i < n;i++)
test.dp[i + 1][1] = in.nextInt();
for(int i = 0;i < n - 1;i++) {
int point1 = in.nextInt();
int point2 = in.nextInt();
test.creatTree(point1, point2);
}
test.dfs(1, 0); //从创建的数的根节点(即第1个顶点,0表示根节点的父母节点)开始进行DFS遍历
int max = (test.dp[1][1] > test.dp[1][0] ? test.dp[1][1] : test.dp[1][0]);
System.out.println(max);
}
}
五、最短路
问题描述
给定一个n个顶点,m条边的有向图(其中某些边权可能为负,但保证没有负环)。请你计算从1号点到其他点的最短路(顶点从1到n编号)。
输入格式
第一行两个整数n, m。
接下来的m行,每行有三个整数u, v, l,表示u到v有一条长度为l的边。
输出格式
共n-1行,第i行表示1号点到i+1号点的最短路。
样例输入
3 3
1 2 -1
2 3 -1
3 1 2
样例输出
-1
-2
数据规模与约定
对于10%的数据,n = 2,m = 2。
对于30%的数据,n <= 5,m <= 10。
对于100%的数据,1 <= n <= 20000,1 <= m <= 200000,-10000 <= l <= 10000,保证从任意顶点都能到达其他所有顶点。
最短路径的问题,有Dijkstra算法和Floyd算法,Bellman-Ford算法和SPFA算法,后三种可以解决负权问题。
Dijkstra算法和Bellman-Ford解决否点到其他各点的最短距离。
我找到了两篇相当不错的文章,来介绍Dijkstra和Floyd算法的(原作者的链接,我找不到了,大家转啊转的也没有标明原作者链接,如果哪位知道,评论到下面吧)
Dijkstra算法:
https://www.cnblogs.com/wangyuliang/p/9216511.html
Floyd算法:
https://www.cnblogs.com/wangyuliang/p/9216365.html
很好理解,Floyd的算法能解决的问题类型相对来说比较多,但复杂度较高,O(n^3)。Dijkstra复杂度在O(n^2),但是Dijkstra不能解决复权问题,虽然题目中说无负环。
Floyd算法:
import java.util.*;
public class Main {
public static void main(String[] args) {
int n, m;
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
int [][]e = new int [n+1][n+1];
for(int i = 1;i<=n;i++){
for(int j = 1;j<=n;j++){
if(i == j)
e[i][j] = 0;
else
e[i][j] = 99999;
}
}
int p1,p2,val;
for(int i = 0;i<m;i++){
p1 = sc.nextInt();
p2 = sc.nextInt();
val = sc.nextInt();
e[p1][p2] = val;
}
sc.close();
for(int k = 1;k<=n;k++)
for(int i = 1;i<=n;i++)
for(int j = 1;j<=n;j++)
if(e[i][j] > e[i][k] + e[k][j])
e[i][j] = e[i][k] + e[k][j];
for(int i = 2; i<=n;i++){
System.out.println(e[1][i]);
}
}
}
复杂度过高,只能过四组数据。
虽然Dijkstra算法不能处理负权问题,但我依旧试了下,结果没想到啊:
import java.util.*;
public class Main {
public static void main(String[] args) {
int n, m;
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
int [][]e = new int [n+1][n+1];
int []dis = new int [n+1];
int []book = new int [n+1];
for(int i = 1;i<=n;i++){
for(int j = 1;j<=n;j++){
if(i == j)
e[i][j] = 0;
else
e[i][j] = Integer.MAX_VALUE;
}
}
int p1,p2,val;
for(int i = 0;i<m;i++){
p1 = sc.nextInt();
p2 = sc.nextInt();
val = sc.nextInt();
e[p1][p2] = val;
}
for(int i = 1;i<=n;i++){
dis[i] = e[1][i];
}
for(int i = 1;i<=n;i++){
book[i] = 0;
}
book[1] = 1;
sc.close();
int min;
int index = 0;
for(int i = 2;i<=n;i++){
min = Integer.MAX_VALUE;
for(int j = 2;j<=n;j++){
if(book[j] == 0 && dis[j]<min){
min = dis[j];
index = j;
}
}
book[index] = 1;
for(int k = 2;k<=n;k++){
if(e[index][k] < Integer.MAX_VALUE){
if(dis[k] > dis[index] + e[index][k]){
dis[k] = dis[index] + e[index][k];
}
}
}
}
for(int i = 2; i<=n;i++){
System.out.println(dis[i]);
}
}
}
竟然还多对了两组,看来测试的案例还挺特殊的,没有普遍性。其他几组没过的是超时,还不是错误。好吧。
这个地方后面我会再补充,现在先到这里。
笔记点:
(1)读入一个string数组:String arr[] = sc.nextLine().toString().split(" ")
(2)复制数组:Arrays.copyOfRange(array, from, to)
(这个系列的我不想写了,纯粹是为了准备这个比赛写的,其实并不经常用java,斟酌过一番后,过了这个就回去找python了,一个玩好比两个不满强吧。最后一篇我们来简单谈下一些比较简单常用的数据结构好吗。)