目录
A纯质数
题目:
题解:
暴力枚举判断即可,最终结果1903
public static void main(String[] args) {
int count = 0;
for (int i = 2; i <= 20210605; i++) {
if (is(i)) count++;
}
System.out.println(count);//1903
}
static boolean is(int n) {
int temp = n;
while (n > 0) {
if (!isDigitPrime(n % 10)) return false;
n /= 10;
}
return isPrime(temp);
}
private static boolean isDigitPrime(int t) {
return t == 2 || t == 3 || t == 5 || t == 7;
}
static boolean isPrime(int n) {
int s = (int) Math.sqrt(n);
for (int i = 2; i <= s; i++) {
if (n % i == 0) return false;
}
return true;
}
B完全日期
题目:
题解:
使用LocalDate枚举日期,判断是否为完全日期
public static void main(String[] args) {
LocalDate s = LocalDate.of(2001, 1, 1);
LocalDate e = LocalDate.of(2021, 12, 31);
int count = 0;
while (s.isBefore(e)) {
int y = s.getYear(), m = s.getMonthValue(), d = s.getDayOfMonth();
if (is(y, m, d)) count++;
s = s.plusDays(1);
}
System.out.println(count);//977
}
static boolean is(int y, int m, int d) {
int sum = getDigitSum(y) + getDigitSum(m) + getDigitSum(d);
int sqrt = (int) Math.sqrt(sum);
return sqrt * sqrt == sum;
}
private static int getDigitSum(int num) {
int sum = 0;
while (num > 0) {
sum += num % 10;
num /= 10;
}
return sum;
}
C最小权值
题目:
题解:
分治:
令f(n)表示n个结点的二叉树的最小权值,则ans=f(2021)
枚举左子树节点个数left,则右子树节点个数right为n-left-1
则f(n) = 1 + 2*f(left) + 3*(right) + left*left*right
因为要选择权值最小的一种,所以f(n) = Min( 1 + 2*f(left) + 3*(right) + left*left*right ) left∈[0,n-1]
对于终止条件,当n==0时,空子树权值为0, 当n==1时,单节点权值为1
public static void main(String[] args) {
System.out.println(f(2021));//2653631372
}
static long[] memo = new long[2022];//记忆化
/**
n个节点的最小权值
*/
static long f(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
if (memo[n] != 0) return memo[n];
long min = 1 + 3 * f(n - 1);//左子树空的情况
for (int left = 1; left < n; left++) {
int right = n - left - 1;
min = Math.min(min, 1 + 2 * f(left) + 3 * f(right) + (long) left * left * right);
}
return memo[n] = min;
}
D覆盖
题目:
题解:
dfs:
令dfs(a,b)表示(a,b)之前的格子均已覆盖的剩余摆放方案数,则ans=dfs(0,0)
- 如果当前格子(a,b)未被覆盖
- 如果右边的格子也未覆盖,那么可以在(a,b),(a,b+1)放置一张横向纸片进行搜索, dfs(a,b+1)
- 如果下边的格子也未覆盖,那么可以在(a,b),(a+1,b)放置一张竖向纸片进行搜索b==7? dfss(a+1,0) : dfs(a,b+1) // 搜索方向一律向右,向右越界则换到下一行开头
2. 如果当前格子(a,b)已被覆盖,则直接搜索下一个位置, b==7? dfss(a+1,0) : dfs(a,b+1)
终止条件为a==8时,此时说明全部覆盖了,count++
static boolean[][] bool = new boolean[8][8];
static int count = 0;
public static void main(String[] args) {
dfs(0, 0);
System.out.println(count);//12988816
}
public static void dfs(int a, int b) {
if (a == 8) {
count++;
return;
}
if (!bool[a][b]) {
bool[a][b] = true;
if (b != 7 && !bool[a][b + 1]) {//横向放置
bool[a][b + 1] = true;
dfs(a, b + 1);
bool[a][b + 1] = false;
}
if (a != 7 && !bool[a + 1][b]) {//竖向放置
bool[a + 1][b] = true;
if (b == 7) dfs(a + 1, 0);
else dfs(a, b + 1);
bool[a + 1][b] = false;
}
bool[a][b] = false;
} else {//不能放置
if (b == 7) dfs(a + 1, 0);
else dfs(a, b + 1);
}
}
E123
题目:
题解:
令S(n)为这个数列的前n项和, 所以[l,r]项的和可以表示为S(r)-S(l-1)
对于这个数列的前n项和:
令 T(n)为数列{1,2,3,4,5,..}的前n项和
S(n) = (1) + (1+2) +(1+2+3) + ... + (1+2+..+m) + (1+2+..+k)
= T(1)+T(2)+...+T(m) + T(k)
T(x)是很好求出的,所以现在只需要得到m和k的确切数值即可
因为 n = 1 + 2 + 3 + ... + m + k = (1+m)*m / 2 + k
则 (1+m)*m / 2 <= n < (1+m+1)*(m+1) / 2
所以可以用二分查找最大的m,满足(1+m)*m / 2 <=n
而k = n - (1+m)*m / 2
T(1)+T(2)+...T(m)对于连续的加项可以使用前缀和表示
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
for (int i = 0; i < T; i++) {
long l = sc.nextLong(), r = sc.nextLong();
System.out.println(SRange(l, r)); // 数列第l项至第r项的和
}
}
static int N = 10000000;
static long[] T = new long[N + 1];// T[n]: 数列{1,2,3,4...}的前n项和
static long[] pre = new long[N + 1];// T的前缀和
static {
for (int i = 1; i < N; i++) T[i] = T[i - 1] + i;
for (int i = 1; i < N; i++) pre[i] = pre[i - 1] + T[i];
}
private static long SRange(long l, long r) {
return S(r) - S(l - 1);
}
/**
S(n) = (1) + (1+2) +(1+2+3) + ... + (1+2+..+m) + (1+2+..+k)
= T(1)+T(2)+...+T(m) + T(k)
*/
private static long S(long n) {
if (n == 0) return 0;
int m = getM(n);
int k = (int) (n - (long) m * (m + 1) / 2);
return pre[m] + T[k];
}
private static int getM(long n) {
// (m+1)m/2 <= n < (m+2)(m+1)/2
int left = 1, right = (int) Math.sqrt(2 * n);// m^2 + m <= 2n --> m < sqrt(2n)
int m = 1;
while (left <= right) {
int mid = (left + right) >>> 1;
if ((long) (mid + 1) * mid / 2 > n) {
right = mid - 1;
} else {
m = mid;
left = mid + 1;
}
}
return m;
}
F二进制问题
题目:
题解:
将n转为二进制, 然后进行数位dp, 枚举出k个1即可
定义f(i,k,isLimit)
- i表示当前枚举的数位是第几个
- k表示还需要枚举的1的数量
-
isLimit表示前面枚举的数位都到达n这个上界
那么当i==len(n)时枚举完了全部数位,如果此时k==0则数有效,count++, 否则数无效
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long n = sc.nextLong();
int k = sc.nextInt();
high = Long.toBinaryString(n);//将n转为二进制
memo = new long[high.length()][k + 1][2];//记忆化
for (int i = 0; i < high.length(); i++) {
for (int j = 0; j <= k; j++) {
Arrays.fill(memo[i][j], -1);//初始化为-1
}
}
System.out.println(f(0, k, true));// 二进制数位dp
}
static String high;
static long[][][] memo;
/**
@param i 当前枚举数位
@param k 剩余可填1的个数
@param isLimit 前面填的数字是否都到达high的上界
*/
static long f(int i, int k, boolean isLimit) {
int n = high.length();
if (i == n) {//已枚举全部数位
return k == 0 ? 1 : 0;//需要恰好为k个1
}
if (memo[i][k][isLimit ? 1 : 0] != -1) return memo[i][k][isLimit ? 1 : 0];//记忆化
int up;
if (k == 0) {//不能填1了
up = 0;
} else {//还能填1
up = isLimit ? high.charAt(i) - '0' : 1;//根据上界确定范围,如果前面的数都触达n上界,那么该位也受到限制,否则无限制可1可0
}
long ans = 0;
for (int j = 0; j <= up; j++) {
ans += f(i + 1, k - (j == 1 ? 1 : 0), isLimit && j == up);
}
return memo[i][k][isLimit ? 1 : 0] = ans;
}
G冰山
题目:
题解(7AC 3TLE):
使用Map映射冰山体积->冰山对应数量, 由于冰山数量非常大,需要使用BigInteger存储
然后根据每一天的情况进行模拟操作
static BigInteger zero = BigInteger.ZERO, one = BigInteger.ONE;
static BigInteger mod = new BigInteger("998244353");
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt(); // 初始冰山数量
int m = scanner.nextInt(); // 观察的天数
int k = scanner.nextInt(); // 冰山的最大限制
// 冰山体积 -> 对应的体积的冰山数量
Map<Integer, BigInteger> VToCount = new HashMap<>();
for (int i = 0; i < n; i++) {
int a = scanner.nextInt();
VToCount.put(a, VToCount.getOrDefault(a, zero).add(one));
}
for (int i = 0; i < m; i++) {
int x = scanner.nextInt(), y = scanner.nextInt(); // 每天冰山的变化量x每天漂来的冰山体积y
VToCount = changeV(VToCount, x, y, k);//模拟操作
BigInteger sum = new BigInteger("0");
// 求每一天的体积之和
Set<Integer> keys = VToCount.keySet();
for (int j : keys) {
sum = sum.add(BigInteger.valueOf(j).multiply(VToCount.get(j)));//v*count
sum = sum.divideAndRemainder(mod)[1];
}
System.out.println(sum);
}
scanner.close();
}
public static Map<Integer, BigInteger> changeV(Map<Integer, BigInteger> VToCount, int x, int y, int k) {
Map<Integer, BigInteger> map = new HashMap<>();//新键一个map,存放操作后的冰山情况
Set<Integer> Vs = VToCount.keySet();
for (int v : Vs) {
BigInteger count = VToCount.get(v);//体积v有count个
long vNext = v + x;//变化后的体积
if (vNext <= 0) continue; //vNext<=0,冰山消失,不存入map
if (vNext <= k) {// 未超出k的限制
BigInteger countNext = map.getOrDefault((int) vNext, zero).add(count);
map.put((int) vNext, countNext);
} else if (vNext > k) { // 超出k的限制,分裂
BigInteger countKNext = map.getOrDefault(k, zero).add(count);//保留1块体积k的冰山
map.put(k, countKNext);
BigInteger bi = BigInteger.valueOf(vNext - k);//每个冰山的其余体积分裂为a-k个体积1的冰山
BigInteger countOneNext = map.getOrDefault(1, zero).add(bi.multiply(count));
map.put(1, countOneNext);
}
}
if (y != 0) {
map.put(y, map.getOrDefault(y, zero).add(one));
}
return map;
}
H和与乘积
题目:
题解:
static long MAX_VALUE = 400_0000_0000L;
static int maxN = 200_0007;
static long[] a = new long[maxN], pre = new long[maxN];
/**
积是成倍增长的,积的增长比和要快很多
唯一的特殊点在于1,乘1不变,和增加
所以根据单调性,对于一段连续的1区间,最多有1个解
前缀和pre[i] = Sum( a[0...i] )
Sum( a[l,r] ) = pre[r] - pre[l-1]
枚举区间左端点l,维护区间乘积res,再枚举区间右端点r,其中r位置的数不为1
x 1...1 y1 1..1 y2 ..
l r1 r2 r3...
(rk-1,rk)上最多有一个解,再检查rk是否是一个解,这样就把所有位置的解求出来了
*/
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
List<Integer> pos = new ArrayList<>();//存储非1数的下标
pos.add(0);//索引从1开始
for (int i = 1; i <= n; i++) {
a[i] = sc.nextInt();
if (a[i] != 1) pos.add(i);
pre[i] = pre[i - 1] + a[i];//前缀和
}
pos.add(n + 1);//区间终点
System.out.println(pos);
long ans = 0;
for (int l = 1; l <= n; l++) {//枚举区间左端点
long res = 1;//区间乘积,如果l位置是1,则初始为1,如果l位置不是1,那么now就是l
int now = 0; //找到下标l之后第一个不为1的数的下标idx,其中idx=pos[now],满足a[idx]!=1 && idx>=l
for (int i = 0; i < pos.size(); i++) {//TODO 二分
Integer idx = pos.get(i);
if (idx >= l) {
now = i;
break;
}
}
for (int j = now; j < pos.size(); j++) {//枚举区间右端点(不为1的数)
int r = pos.get(j);
int cnt = pos.get(j) - pos.get(j - 1) - 1;//前一个不为1的数到当前不为1的数的区间上1的个数
long m = pre[r - 1] - pre[l - 1];
if (res <= m && m - cnt < res) { //如果连续的1区间内有解
//m>=res:区间总和大于(等于)乘积; m-cnt<res:区间总和减去1的个数小于乘积 ==> 根据单调关系,必然存在一个位置有解
ans++;
}
res = res * a[r];
if (r != n + 1 && res == pre[r] - pre[l - 1]) { //当前不为1的位置 是一个解
//r!=n+1:n+1是数组越界位置; res == pre[r] - pre[l - 1]:区间乘积等于区间乘积
ans++;
}
if (res >= MAX_VALUE) break;//res太大了,可以提前退出
}
}
System.out.println(ans);
}
I异或三角
题目:
题解:
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
for (int i = 0; i < T; i++) {
int n = sc.nextInt();
System.out.println(solve(n) * 3);//三元组位置轮换
}
}
static long solve(int n) {
for (int i = 0; i < 32; i++) {
for (int j = 0; j < 2; j++) {
Arrays.fill(dp[i][j], -1);
}
}
int cnt = 0;
while (n > 0) {
cnt += 1;
num[cnt] = n & 1;
n >>= 1;
}
return dfs(cnt, true, 0);
}
/**
设 n >= a >= b >= c >= 1 <br>
abc每个数位都有2个1,1个0 <br>
1. a>b, 所以(ai,bi)=(1,0)必须出现在(aj,bj)=(0,1)前面 <br>
a ? 0 1 <br>
b ? 1 0 -> a的?处必然需要填1,而剩下的1无论分给b还是c都会导致a>b&&a>c不成立 <br>
c ? 1 1 <br>
<br>
2. a>c, 所以(ai,bi)=(1,1)必须出现在(aj,bj)=(0,1)前面 <br>
a ? 0 1 <br>
b ? 1 1 -> 同理: a的?处必然需要填1,而剩下的1无论分给b还是c都会导致a>b&&a>c不成立 <br>
c ? 1 0 <br>
<br>
3. a = b^c < b+c,所以必然存在状态(ai,bi)=(0,1) <br>
因为b^c是不进位加法,0^0=0+0,0^1=0+1,唯有(bi,ci)=(1,1)时 bi^ci=0 < bi+ci <br>
<br>
所以当枚举到a的最后,(0,1),(1,0),(1,1)都存在时,则为有效情况 <br>
// 因为(0,1)必然出现,又有(0,1)出现时,前面一定有(1,0)和(1,1) <br>
@param i 当前a所在的二进制位数
@param limit 前面所选择的数是否全部为上限数
@param states 是否出现过上述三种状态 1->(0,1),2->(1,0),4->(1,1)
@return
*/
static long dfs(int i, boolean limit, int states) {
if (i == 0) return states == 7 ? 1 : 0;
int idx = limit ? 1 : 0;
if (dp[i][idx][states] != -1) return dp[i][idx][states];
int up = limit ? num[i] : 1;
long res = 0;
for (int d = 0; d <= up; d++) {
if (d == 0) { //如果该位置上a为0
//1. (0, 0, 0)
res += dfs(i - 1, limit && d == up, states);
//2. (0, 1)
if (states >= 6) //选(0, 1) 时,(1, 1),(1, 0)必须要出现
res += dfs(i - 1, limit && d == up, states | 1);// 110 | 001 = 111
} else {// d == 1
//3. (1, 0)
res += dfs(i - 1, limit && d == up, states | 2); //x0x | 010 = x1x
//4. (1, 1)
res += dfs(i - 1, limit && d == up, states | 4); //0xx | 100 = 1xx
}
}
return dp[i][idx][states] = res;
}
static long[][][] dp = new long[32][2][8];
static int[] num = new int[32];
J积木
题目:
题解:
令f[i]表示水高为i时被水淹的积木数
对于一个高度h的积木,它对 f [1~h] 都有1点贡献, 对于区间加问题,可以使用差分数组解决
差分数组(详解):
举例:
考虑数组 a=[1,3,3,5,8],对其中的相邻元素两两作差(右边减左边), 得到数组 [2,0,2,3]。
然后在开头补上 a[0],得到差分数组 d=[1,2,0,2,3]
这有什么用呢? 如果从左到右累加 d 中的元素,我们就「还原」回了 a 数组 [1,3,3,5,8]。 这类似求导与积分的概念。
这又有什么用呢? 现在把连续子数组 a[1],a[2],a[3] 都加上 10,得到 a′=[1,13,13,15,8]。
再次两两作差,并在开头补上 a′[0],得到差分数组d′=[1,12,0,2,−7]
对比 d = [1,2,0,2,3] 和 d′ = [1,12,0,2,−7] , 可以发现只有 d[1] 和 d[4] 变化了 , 这意味着对 a 中连续子数组的操作,可以转变成对差分数组 d 中两个数的操作 。
定义和性质:
对于数组 a,定义其差分数组为 d[i]={ a[0],i=0 ; a[i]−a[i−1],i≥1 }
性质 1:从左到右累加 d 中的元素,可以得到数组 a。
性质 2:如下两个操作是等价的。
操作1: 把 a 的子数组 a[i],a[i+1],⋯,a[j] 都加上 x。
操作2: 把 d[i] 增加 x,把 d[j+1] 减少 x。
利用性质 2,我们只需要 O(1)) 的时间就可以完成对 a 的子数组的操作。最后利用性质 1 从差分数组复原出数组 a。 !注:也可以这样理解,d[i] 表示把下标 ≥i 的数都加上 d[i]。
static int N = 100000;
static StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));// 必须使用输入优化,否则有测试点会超时
static int Int() {
try {
st.nextToken();
} catch (Exception ignored) {
}
return (int) st.nval;
}
public static void main(String[] args) {
int n = Int(), m = Int();
int[] d = new int[N + 1];//差分数组
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
int h = Math.min(Int(), N + 1);
if (h == 0) continue;
//f[1~h]+1 <=> d[1]+1,d[h+1]-1
d[1]++;
d[h + 1]--;
}
}
int H = Int();
for (int i = 1; i <= H; i++) {//一次前缀和还原数组f
d[i] += d[i - 1];
}
long sum = 0;
for (int i = 1; i <= H; i++) {//求前缀和
sum += d[i];
System.out.println(sum);
}
}