A裁纸刀
题目
题解
记忆化搜索
定义f(x,y)为x行y列至少裁的次数
- 如果选择在第i行上裁, 有转移方程f(x,y) = 1+ f(i,y)+f(x-i,y) , 第i行裁一次,剩余两边的子问题
- 如果选择在第j列上裁, 有转移方程f(x,y) = 1+ f(x,j)+f(x,y-j) , 第j列裁一次,剩余两边的子问题
因为需要次数最少,所以需要对dfs(x,y)取min
当只有一行或者一列时, 只需要裁剩余二维码个数-1即可
最后答案为dfs(20,22)+4, 需要加上整张纸边缘的四刀
public static void main(String[] args) {
for (int i = 0; i < 25; i++) {
Arrays.fill(memo[i], -1);
}
System.out.println(4 + f(20, 22));// 443, 边缘需要额外的4刀
}
static int[][] memo = new int[25][25];//记忆化数组
/**
x行y列需要裁多少刀
*/
static int f(int x, int y) {
//一行一列,一个一个裁
if (x == 1) return y - 1;
if (y == 1) return x - 1;
if (memo[x][y] != -1) return memo[x][y];
//按行
int min = Integer.MAX_VALUE;
for (int i = 1; i < x; i++) {
min = Math.min(min, 1 + f(i, y) + f(x - i, y));
}
//按列
for (int i = 1; i < y; i++) {
min = Math.min(min, 1 + f(x, i) + f(x, y - i));
}
return memo[x][y] = min;
}
B寻找整数
题目
题解
首先有一条性质: 如果 a % b = c, 那么对a加上b的任意倍数,a % b的结果不变, 即 a % b = (a+kb) % b
令 x[i] 表示 满足对前i个数取余的结果都能对应余数表的 最小正整数, X[i] 表示 满足对前i个数取余的结果都能对应余数表的 通解
那么有关系式 X[i] = x[i] + k1 * lcm(m1,m2,m3,...,mi)
X[i+1] = x[i+1] + k2 * lcm(m1,m2,...mi+1)
因为只需要取到某对(k1,k2), 可以使通解相等, 那么两式联立可得:
x[i] + k1 * lcm(m1,m2,m3,...,mi) = x[i+1] + k2 * lcm(m1,m2,...mi+1)
k1 * lcm(m1,m2,m3,...,mi) 与 k2 * lcm(m1,m2,...mi+1) 的公因子为lcm(m1,m2,m3,...,mi)
所以 x[i+1] = x[i] + t * lcm(m1,..,mi)
其中需要满足 x[i+1] % i == map[i], t为不定参数,枚举t,使x[i+1]满足要求即可
这是一个递推式, 递推项为x和lcm
x[i+1] = x[i] + t * lcm[i] // lcm[i] = lcm(m1,m2,..mi)
lcm[i] = lcm(lcm[i-1], i)
public static void main(String[] args) {
//map[i]: x mod i的值
int[] map = new int[]{0, 0, 1, 2, 1, 4, 5, 4, 1, 2, 9, 0, 5, 10, 11, 14, 9, 0, 11, 18, 9, 11, 11, 15, 17, 9, 23, 20, 25, 16, 29, 27, 25, 11, 17, 4, 29, 22, 37, 23, 9, 1, 11, 11, 33, 29, 15, 5, 41, 46};
long x = 1;//x[i]:满足前i个约束的最小x
long lcm = 1;//lcm[i]:前i个约束的最小公倍数
for (int i = 2; i < map.length; i++) {
while (x % i != map[i]) {
x += lcm; // x[i] = x[i-1] + t * lcm => x[i] % i == map[i]
}
lcm = lcm(lcm, i);
}
System.out.println(x);//2022040920220409
}
static long lcm(long a, long b) {
return a * b / gcd(a, b);
}
static long gcd(long a, long b) {
if (b == 0) return a;
return gcd(b, a % b);
}
C求和
题目
求[1,20230408]的和
题解
送分题
public static void main(String[] args) {
System.out.println((1 + 20230408L) * 20230408 / 2);
}
DGCD
题目
题解
首先根据gcd(x,y)=gcd(x-y,y)做一个变换 gcd(a+k,b+k)=gcd(a-b,b+k)
gcd(a-b,b+k)能取到的最大值等于a-b, 当(b+k)是(a-b)的倍数时
所以只需要求出满足 (b+k) % (a-b)== 0 的最小k
设 (b+k) / (a-b) = t
那么有 b+k = t(a-b) >= b
所以求 满足(a-b)* t > b 的最小t, 则 k = (a-b)*t - b
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long a = sc.nextLong(), b = sc.nextLong();
if (b > a) {//交换ab,保证a>=b
long t = a;
a = b;
b = t;
}
long diff = a - b; // 求 (b+k) % diff == 0 的最小k
int t = (int) (b / diff + 1);// 求 diff * t > b 的最小t, 则 k=diff*t-b
System.out.println(diff * t - b);
}
E蜂巢
题目
题解
对坐标进行数学变换即可
例如上图的B和C, C坐标可以表示为(2.5 , 1) ,B的坐标可以表示为(-3.5, 3)
与我们熟悉的平面直角坐标系有一点不同就是,按y轴移动时,x轴坐标也会改变
比如说C点要移动到原点O, 它与原点O的y轴差为1, 从C点往下一格, 坐标变为(2,0),再向左两步可到达原点O
也就是说, 两点y轴差的一半可以给x轴距离进行缩减
BC的距离y轴差为2, x轴差为6, 移动2步y, x轴距离减1,x轴差变为5, 最终步数为2+5=7
步数公式为 x轴要走的步数 + y轴要走的步数 = (diffX - diffY/2) + diffY = diffX + diffY/2
如果 diffY/2 > diffX: 那么说明一个点处于另一个点y轴最大x缩减范围上, 保持下降y轴的同时, 此时可以控制方向, 将x轴差控制在0.5的范围内, x轴不需要再另外消耗步数
最终公式是 max(0, diffX - diffY / 2) + diffY
static double[][] direction = new double[][]{
{-1, 0}, {-0.5, 1}, {0.5, 1}, {1, 0}, {0.5, -1}, {-0.5, -1}
};//移动方向与xy坐标的变换 *斜向移动会使横向距离改变0.5长度
/**
数学坐标变换
*斜向移动两步可以缩减一格的横向距离
ans = 横向移动diffX-diffY/2 + 纵向移动diffY
*/
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int d1 = sc.nextInt(), p1 = sc.nextInt(), q1 = sc.nextInt();
int d2 = sc.nextInt(), p2 = sc.nextInt(), q2 = sc.nextInt();
double[] point1 = change(d1, p1, q1);//转换为(x,y)坐标
double[] point2 = change(d2, p2, q2);
double diffX = Math.abs(point1[0] - point2[0]);
double diffY = Math.abs(point1[1] - point2[1]);
System.out.println((int) (Math.max(0, diffX - diffY / 2) + diffY));
}
static double[] change(int d, int p, int q) {
double x = 0, y = 0;
double[] dir = direction[d];
x += p * dir[0];
y += p * dir[1];
dir = direction[(d + 2) % 6];
x += q * dir[0];
y += q * dir[1];
return new double[]{x, y};
}
F全排列的价值
题目
题解
对于某个排列 a1 a2 a3 ... ak ... an 和它的反排列 an, an-1,...ak,...a2,a1
现在考虑这两次排列ak的总价值,设ak=v
设 a[1,k-1]中 小于v的有x个数, a[k+1, n] 小于v的有y个数
因为序列由[1,n]组成,那么总共小于ak=v的数就有v-1个
所以x+y=v-1 即 正排列+反排列ak的贡献为v-1
全体元素的贡献为 Sum(ak - 1) ,等于 [0,n-1]的和, n(n-1)/2
也就是说一对正反排列的价值为n(n-1)/2
排列数A(n,n)=n!, 总共有 n!/2对正反排列
所以价值为 n(n-1)/2 * n!/2
因为结果需要对mod取余, 而式子里有除法,除法不适用模性质,这里有三种处理方式(见代码注释)
/**
sum(n) = n * (n-1) * n! / 4
法1: 不乘就是除了
ans = sum(n) % mod
= n * (n-1) / 2 % mod * mul(3,n) % mod // n * (n-1)有2的因子可以直接除, n!部分不乘2
法2: 模上的除法提取到模外
ans = n * (n-1) * n! / 4 % mod
= n * (n-1) * n! % (4mod) / 4 // 公式 (a / b) % mod = (a % (b*mod)) /b
= n * (n-1) % (4mod) * n! % (4mod) / 4
法3: 乘法逆元,乘法代替除法
ans = n * (n-1) * n! / 4 % mod
= n * (n-1) * n! * inv(4) % mod // inv(4)表示在mod下的4的乘法逆元, inv(4)=4^(mod-2)
= n * (n-1) % mod * n! % mod * inv(4) % mod
*/
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long n = sc.nextInt();
long ans = n * (n - 1) / 2 % mod;
for (int i = 3; i <= n; i++) {//2不乘,相当于结果除以2
ans = (ans * i) % mod;
}
System.out.println(ans);
}
G青蛙过河
题目
题解
假设青蛙的步长为y
如果需要往返x次, 那么对于任意一个长度为y的区间, 区间和必须大于等于2x
如果长度为y的某个区间的和小于2x,那么这个区间就无法支撑x次往返,步长y就不能作为答案
如果y满足了以上条件, 那么一定能够往返x次
因为可以找到一个划分, 使得每个区间的和都大于等于2x,这样每次的按照区间去跳即可
所以可以二分枚举步长,检查在该步长下是否所有区间和都大于等于2x
检查固定长度的子数组和,可以使用前缀和数组快速查询
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(), x = Int();
//前缀和
long[] preSum = new long[n];
for (int i = 1; i < n; i++) {
int h = Int();
preSum[i] = h + preSum[i - 1];
}
//二分枚举步长
int l = 0, r = n;
while (l + 1 != r) {
int mid = (l + r + 1) >> 1;
if (check(preSum, mid, x)) {
r = mid;
} else {
l = mid;
}
}
System.out.println(r);
}
/**
检查长度为y的h子数组和是否都大于等于2x
@param preSum h数组的前缀和,preSum[i] = sum( h[0,i) )
@param y 步长
@param x 往返次数
*/
private static boolean check(long[] preSum, int y, int x) {
int n = preSum.length;
long _2x = 2L * x;
for (int i = 0; i + y < n; i++) {// preSum[i + y] - preSum[i] = h[i,i+y)
if (preSum[i + y] - preSum[i] < _2x) return false;
}
return true;
}
H因数平方和
题目
题解
考虑1出现次数, 每个数都有因子1, 次数a1=n
考虑2出现次数, 每两个数有一个因子2, 次数a2 = floor(n/2)
考虑3出现次数, 每三个数有一个因子3, 次数a3 = floor(n/3)
...
所以
对于 这样的式子,可以使用数论分块提高运行效率, 前提是f(x)可以快速地求部分和,或者已经预处理出f(x)的前缀和数组了, 这里为k^2,前n个数的平方和公式为n(n+1)(2n+1)/6
数论分块
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
floor(n/i) | 12 | 6 | 4 | 3 | 2 | 1 | ||||||
left | right |
在n=12时,floor(n/i)可以分块为: {1},{2},{3},{4},{5,6},{7,8,9,10,11,12}
对于Sum(floor(n/i))可以分别对每个块进行处理, 如上图n=12,原本需要遍历12个,现在分为了6块,只需要遍历6个即可
对于某个块的左端点left, 如何确定它的块大小, 即块的右端点right的位置
令
那么有
则
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
long ans = 0;
for (int l = 1; l <= n; ) {//处理所有块[l,r]
int x = n / l, r = n / (n / l);
//x = floor(n/l) ; s(r)-s(l-1) = sum(i^2) l<=i<=r
ans = ((ans + x * (s(r) - s(l - 1))) % mod + mod) % mod;
l = r + 1;
}
System.out.println(ans);
}
static int mod = 10_0000_0007;
static final int INV = 166666668;//6的乘法逆元
/**
sum(i^2) 1<=i<=n
*/
static long s(long n) {
return n * (n + 1) % mod * (2 * n + 1) % mod * INV % mod;
}
I最优清零方案
题目
题解
- 区间操作一定优于单点操作
- 对于区间[l,r], 假设其中的最小值为min, 则最多可以对[l,r]执行min次操作
假设[0,i]都已变为0, 对于区间[i+1,N), 选择[i+1,i+k]执行 A[j] = min 次操作, 则操作之后 A[j]变为0, [i+1,j-1]长度不足k, 只能单点修改, 剩余[j+1,N)的子问题
对于[0,k]这个区间,设[0,k-1]的最小值A[j]=min, 考虑对[0,k-1]操作和对[1,k]操作
case1: j!=0
1. [0,k-1]最多操作min次, 而[1,k]由于加入了A[k], 去掉了A[0] (A[0]>A[j]) , A[k]可能比min更小,[1,k]的可操作次数就小于等于min次
2. 如果选择操作[1,k], 那么位置0不能区间操作, 只能进行单点修改
综合这两点:[0,k-1]的操作优于[1,k]的操作
case2: j==0 设[0,k-1]的第二小值A[t]=sec
1. 如果先对[0,k-1]操作,可操作min次, 再对[1,k]操作,可操作sec-min次, 总操作次数为sec
2.如果先对[1,k]操作,可操作sec次, 0位置不能再操作,单点修改A[0]次, 总操作次数为sec+A[0] 所以对[0,k-1]操作优于对[1,k]操作
贪心:
从前往后枚举区间, 进行区间操作, 最大操作次数为min, min累加到ans中
每次操作后当前区间不能再进行操作了, 因为当前区间有元素变为0, 此时需要跳转到下一个区间
下一个区间为当前区间的最后一个0位置的下一位
最后剩余的数字都是需要单点修改的,将其累加到ans中
public static void main(String[] args) {
int N = Int(), K = Int();
int[] A = new int[N];
for (int i = 0; i < N; i++) A[i] = Int();
long ans = 0;
int left = 0;
while (left + K <= N) {
//对[left,right]进行区间操作
int right = left + K - 1;
//求[left,right]的最小值
int min = Integer.MAX_VALUE;
for (int i = left; i <= right; i++) {
min = Math.min(min, A[i]);
}
//最多操作min次
if (min != 0) {
ans += min;
for (int i = left; i <= right; i++) {
A[i] -= min;//[left,right]区间都减去min TODO 线段树优化, N<1e6, O(n^2)咋能过的
}
}
//[left,min]都无法再进行区间操作,left需要跳转到min+1位置进行区间操作
//left跳转到idx[min]+1位置,如果有多个min,跳转到最后的一个
for (int i = left; i <= right; i++) {
if (A[i] == 0) left = i;
}
left++;
}
for (int i : A) ans += i;//剩下的单点修改
System.out.println(ans);
}
J推导部分和
题目
题解
假如现在已知 sum(a,b)和sum(b+1,c), 那么就可以推导出sum(a,c)
我们定义: sum(a,b)已知 等价于 a-1与b连通
若a与b连通,b与c连通,则 sum(a+1,c) = sum(a+1, b) + sum(b+1,c) , 即a与c连通
带权并查集
所以可以维护一个并查集, 根据给出的部分和做节点连通, 这样在询问时只需要查看节点是否连通即可判断能否推导部分和
如果能推导,我们还需要输出部分和为多少, 那么我们还需要维护一个权值, 即带权并查集
在并查集中数组fa[i]=j 表示 有i->j的连通关系, 令数组 val[i] 表示 i->j的权值,这个权值就是sum(i+1,j)
并查集的基本操作find:
find函数查找元所在集合的集合根
a->b->c 假如 a->b 的权值为x, b->c的权值为y, 即 sum(a+1,b)=x, sum(b+1,c)=y, 那么有sum(a+1,c) = x+y, 所以 a->c 权值为 x+y
所以在find(x)路径压缩时, 如果x的集合根为rx
那么路径压缩fa[x]=rx, 权值压缩val[x]=路径上的边权之和, 因为find函数是递归的, 所以回溯做累加即可, val[x]+=val[oldFa] 原边加上父边
public int find(int x) {
if (x == fa[x]) return x;//x为根
int oldFa = fa[x];//存一下父节点
int root = find(fa[x]);//向父节点走,找集合根
fa[x] = root;//路径压缩
val[x] += val[oldFa];//权值压缩, 回溯时oldFa累加了从oldFa到root的值,现在累加到x上
return root;
}
并查集的基本操作union:
将节点x和y连通, 即合并x和y所在的两个集合, 合并集合需要按根节点合并, 否则直接让x简单指向y,只是将x从原集合移除并添加到y的集合, 原连通性改变, 正确做法是将集合x的根rx指向集合y的根
假设当前 x所在集合的集合根为rx, y所在集合的集合根为ry, 现在需要连通x和y, x->y权值为v
因为是按根节点合并, rx需要去指向ry, fa[rx]=ry
但rx->ry是未知量,下面推导rx->ry的权值
x到ry有两条路径, x->rx->ry 和 x->y->ry, 但x到ry的权值是唯一的,所以可得等式:
x->rx + rx->ry = x->y + y->ry
所以 rx->ry = x->y + y->ry - x->rx
val[rx] = v + val[y] - val[x]
public void union(int x, int y, long v) {//合并节点x和y,x->y权值为v
int rx = find(x), ry = find(y);//查找所在集合根
fa[rx] = ry;//合并集合
val[rx] = val[y] - val[x] + v;//推导rx->ry
}
并查集的基本操作isConnect:
判断两个节点是否连通, 只需要查看这两个节点的集合根是否相同即可
public boolean isConnect(int x, int y) {
return find(x) == find(y);
}
本题额外操作query:
val[i]表示 (i+1)->root的和 sum([i+1,root]) , 现在求[left,right]的和
sum([left,right]) = sum([left,root]) - sum([right+1,root]) = val[left-1] - val[right]
public long query(int left, int right) {
return val[left - 1] - val[right];
}
解题代码
static StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static int Int() {
try {
st.nextToken();
} catch (Exception ignored) {
}
return (int) st.nval;
}
static long Long() {
try {
st.nextToken();
} catch (Exception ignored) {
}
return (long) st.nval;
}
/**
若sum(A[x,y])已知,则x-1与y连通
若a与b连通,b与c连通, 因为 sum(A[a+1,c]) = sum(A[b+1,c]) + sum(A[a+1,b]) ,可得a与c连通
所以维护一个并查集,若i与j连通,则可推导[i,j]部分和,否则无法推导
*/
public static void main(String[] args) {
int n = Int(), m = Int(), q = Int();
Set set = new Set(n);//并查集
//m个部分和
for (int i = 0; i < m; i++) {
int l = Int(), r = Int();
long s = Long();
set.union(l - 1, r, s);// sum(a[l,r])=s
}
//q次询问
for (int i = 0; i < q; i++) {
int l = Int(), r = Int();
if (!set.isConnect(l - 1, r)) System.out.println("UNKNOWN");
else System.out.println(set.query(l, r));
}
}
static class Set {
int[] fa;//fa[i]=j,表示i与j为同一个集合,j比i高,如果i==j,说明i是集合里最高的(根)
long[] val; // fa[i]=j && value[i]=x 表示i->j的权值为x
public Set(int n) {
fa = new int[n + 1];
val = new long[n + 1];
for (int i = 0; i <= n; i++) {
fa[i] = i;
}
}
public int find(int x) {
if (x == fa[x]) return x;
int oldFa = fa[x];
int root = find(fa[x]);
fa[x] = root;
val[x] += val[oldFa];//回溯时oldFa累加了从oldFa到root的值,现在累加到x上
return root;
}
public void union(int x, int y, long v) {
int rx = find(x), ry = find(y);
fa[rx] = ry;
val[rx] = val[y] - val[x] + v;
}
public boolean isConnect(int x, int y) {
return find(x) == find(y);
}
/**
本题应用: val[i]表示 i->root 的和
要求数组[left,right]的和,
sum([left,right])
= sum([left,root]) - sum([right+1,root])
= val[left-1] - val[right]
*/
public long query(int left, int right) {
return val[left - 1] - val[right];
}
}