1230. K倍区间(2月1日)
分析
题目要求统计下所有的区间, 问有多少个区间使得和是K的倍数
直接做 O(n^2)会超时
统计下前缀和
s
i
s_i
si
假设以
i
i
i为右端点的区间已经固定, 那么就是求
i
i
i前面的数
j
j
j, 使得
s
i
−
s
j
s_i - s_j
si−sj是k的倍数
举个例子:
如果区间长度是1, 那是就是问
s
i
−
s
i
−
1
≡
0
(
m
o
d
k
)
s_i - s_{i - 1} \equiv 0(\mod k)
si−si−1≡0(modk) 是否成立
{
s
i
−
s
i
−
1
≡
0
s
i
−
s
i
−
2
≡
0
.
.
.
s
i
−
s
0
≡
0
\left\{\begin{matrix} s_i - s_{i - 1} \equiv 0 \\ s_i - s_{i - 2} \equiv 0 \\ ...\\ s_i - s_{0} \equiv 0 \\ \end{matrix}\right.
⎩⎪⎪⎨⎪⎪⎧si−si−1≡0si−si−2≡0...si−s0≡0
换句话说,
s
0
,
s
1
,
.
.
.
s
i
−
1
s_0, s_1, ... s_{i - 1}
s0,s1,...si−1有多少个数与
s
i
s_i
si 模k的余数的相同
即, 有多少个数与
s
i
s_i
si mod k余数相同
那么可以用hash表存储前面数mod k余数, 每种余数有多少个
数据比较小, 可以开cnt[x], 表示余数为x的前缀和有多少个
当我们要求第i个数的时候, 求s_i % k的余数的个数, 那么直接求cnt[s_i % k]
, 然后将si%k放入到数组中, cnt[si % k] ++
另外要注意一点是,假如某一段数A1…AiA1…Ai,不需要减去区间,它本身就是一个KK倍区间,但cnt[0]cnt[0]保存的是ii之前余数为00的前缀和的个数,这个区间本身没有被算进去,所以初始状态下,cnt[0]应该赋值为1
cnt[0] = 1, 如果当前区间%k == 0, 那么因为cnt[0]已经赋值为1, 就会将当前区间算进去, 然后cnt[0] ++, 同样对于下一个区间如果cnt[s(i + 1) % k] != 0, 那么没事, 如果==0, 那么因为同样因为cnt[0] = 1, 所以当前区间也会被计算到答案里
时间复杂度O(n)
code
#include <iostream>
using namespace std;
const int N = 100010;
typedef long long LL;
LL s[N];
int n, k;
int cnt[N];
int main(){
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i ++ ){
scanf("%lld", &s[i]);
s[i] += s[i - 1];
}
LL res = 0;
cnt[0] = 1;
for (int i = 1; i <= n; i ++ ){
res += cnt[s[i] % k];
cnt[s[i] % k] ++;
}
printf("%lld\n", res);
return 0;
}
1613. 数独简单版(2月2日)
分析
先读入原数独, 标记下每行每列每个3x3小方格已经存在的数
然后从(0, 0)为位置开始dfs, 直到扫描到x = 9,(因为x = 0开始, 其实扫了10行了), 表示填满了, 打印输出即可
code
可以将读入和记录原数独已有的数放到同一循环里
#include <iostream>
using namespace std;
const int N = 10;
char g[N][N];
bool row[N][N], col[N][N], cell[3][3][N];
bool dfs(int x, int y){
if (y == 9) x ++, y = 0;
if (x == 9){
for (int i = 0; i < 9; i ++ ) cout << g[i] << endl;
return 0;
}
if (g[x][y] != '.') return dfs(x, y + 1);
for (int i = 0; i < 9; i ++ ){
if (!row[x][i] && !col[y][i] && !cell[x / 3][y / 3][i]){
g[x][y] = i + '1';
row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = 1;
if (dfs(x, y + 1)) return true;
row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = 0;
g[x][y] = '.';
}
}
return false;
}
int main(){
for (int i = 0; i < 9; i ++ ){
cin >> g[i];
for (int j = 0; j < 9; j ++ )
if (g[i][j] != '.'){
int t = g[i][j] - '1';
row[i][t] = col[j][t] = cell[i / 3][j / 3][t] = 1;
}
}
dfs(0, 0);
return 0;
}
122. 糖果传递(2月3日)
分析
数据范围100000, 时间复杂度要控制在nlogn以内
a
i
a_i
ai:表示第i个小朋友有的糖果数量
x
i
x_i
xi: i - 1给i的的糖果数量
n个方程, 推导发现自由度并不是n, 因为将前面n - 1个方程➕到最后一个方程上, 得到最后一个方程是个恒等式
因此方程是没有唯一解的
然后将推导得到的式子代入
∣
x
1
∣
+
∣
x
2
∣
+
.
.
.
+
∣
x
n
∣
|x_1|+ |x_2|+ ... + |x_n|
∣x1∣+∣x2∣+...+∣xn∣
发现问题转化为货仓选址的问题
现在只是求出了理论最小值, 还需要验证是否存在方案可以取到理论最小值
为什么会有取不到最小值的情况, 比如a3只有5颗糖, 但a3要给a4 10颗糖, 那么就会无解
因为刚刚只是证明了 ∑ ∣ x i ∣ ≥ y \sum |x_i| \geq y ∑∣xi∣≥y, 能不能取到 y y y还不知道
环中不可能存在所有
x
i
>
0
x_i > 0
xi>0, 如果存在这种情况, 可以让所有
x
i
−
1
x_i - 1
xi−1, 可以使得糖果均分, 并且
∑
∣
x
i
∣
\sum|x_i|
∑∣xi∣最小
同理不可能所有
∣
x
i
∣
<
0
|x_i| < 0
∣xi∣<0
因此, 必然存在某些
x
i
≥
0
,
x
i
<
0
x_i \geq 0, x_i < 0
xi≥0,xi<0, 必然存在一个位置, 使得左边
x
i
≥
0
x_i \geq 0
xi≥0, 右边
x
i
<
0
x_i < 0
xi<0
意味着这个小朋友会给左右两边糖果
从这个地方断开, 那么第1个和最后一个小朋友是图中圈出来的同一个小朋友, 既会往左边给, 又会往右边给
来构造一种方案, 使得刚才上述所说的最小值方案能够构造出来
从前往后依次看 x k x_k xk,
- x k ≥ 0 x_k \geq 0 xk≥0 立即给
- x k < 0 x_k < 0 xk<0 最后给
这是一个递归的过程
dfs(int k){
if xk >= 0{
a_k->a_{k + 1}; // 1.需证明
dfs(k + 1);
}else {
dfs(k + 1);
a_{k + 1} -> a_{k}; // 2.需证明
}
}
这样就构造出了一种方案, 那么只要证明这种方案合法就可以了
证明合法等价于代码中1的部分a_k->a_{k + 1};
一定有a_k >= x_k
, 即: 有的糖果数一定要比能给的糖果数多
同理, 代码中2的部分 a_{k + 1} -> a_{k}
, 要证明a_{k + 1} >= x_k
, 往回给的话, 也要有这么多钱能给
下面证明这种给法都是满足要求的, 这样的话就构造出来一种方案, 使得方案一定可以被构造出来, 原来的最小值一定可以被取出来
所以原问题的最优解一定是那个最小值
证明: 最小方案的存在性
x k − 1 ≥ 0 x_{k - 1} \geq 0 xk−1≥0
(因为 a k − 1 a_{k - 1} ak−1在 a k a_k ak的左半边, 上面已经证明了前面的小朋友一定是会给后面的小朋友糖果, 即 x 1 , . . x k − 1 > 0 x_1, .. x_{k - 1} > 0 x1,..xk−1>0)
那么说明在操作 x k − 1 x_{k - 1} xk−1之前 a 1 + . . . + a k − 1 > ( k − 1 ) b a_1 + ... + a_{k - 1} > (k - 1)b a1+...+ak−1>(k−1)b, 那么操作完之后 a 1 + . . . + a k − 1 = ( k − 1 ) b a_1 + ... + a_{k - 1} = (k - 1)b a1+...+ak−1=(k−1)b
yxc:老师上课说是 a 1 + . . . + a k − 1 ≤ ( k − 1 ) b a_1 + ... + a_{k - 1} \leq (k - 1)b a1+...+ak−1≤(k−1)b 有点不理解???
按理说已经操作完成了, 那么就表示可以k - 1个小朋友已经到达平均值了
然后分析 x k ≥ 0 x_k \geq 0 xk≥0, 那么说明前k个小朋友数量比平均值多 a 1 + . . . + a k − 1 + a k ≥ k b a_1 + ... + a_{k - 1} + a_{k} \geq kb a1+...+ak−1+ak≥kb
yxc老师上课这里讲的是 a 1 + . . . + a k − 1 + a k = k b a_1 + ... + a_{k - 1} + a_{k} = kb a1+...+ak−1+ak=kb ???
看看现在有的不等式关系
a
1
+
.
.
.
+
a
k
−
1
=
(
k
−
1
)
b
a_1 + ... + a_{k - 1} = (k - 1)b
a1+...+ak−1=(k−1)b
a
1
+
.
.
.
+
a
k
−
1
+
a
k
≥
k
b
+
x
k
a_1 + ... + a_{k - 1} + a_{k} \geq kb + x_k
a1+...+ak−1+ak≥kb+xk
可以得到
a
k
≥
b
+
x
k
a_k \geq b + x_k
ak≥b+xk, 因为
b
>
0
b > 0
b>0, 所以
a
k
>
x
k
a_k > x_k
ak>xk, 能够给, 合法
从后面往前给的话, 如果
a
k
+
1
a_{k + 1}
ak+1需要给
a
k
a_k
ak
x
k
x_k
xk的话, 那么说明
a
k
+
2
,
.
.
.
a
n
a_{k + 2}, ... a_n
ak+2,...an是恰好满的, 并且
a
k
+
1
,
.
.
.
.
a
n
a_{k + 1}, .... a_n
ak+1,....an是多了
∣
x
k
∣
|x_k|
∣xk∣个
此时
a
k
+
1
=
b
+
∣
x
k
∣
≥
∣
x
k
∣
a_{k + 1} = b + |x_k| \geq |x_k|
ak+1=b+∣xk∣≥∣xk∣
因此就证明了方案的合法性, 并且可以取到最小值
code
代码中先求每一个
c
i
c_i
ci
要求的
x
x
x就是c的中位数
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1000010;
LL s[N], c[N];
int n;
int main(){
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ){
scanf("%lld", &s[i]);
s[i] += s[i - 1];
}
LL b = s[n] / n;
int k = 0;
for (int i = 1; i < n; i ++ ) c[k ++ ] = i * b - s[i]; // c_k 从k = 0, 第0个位置填写
c[k ++] = 0; // 注意最后一个位置c[k] = 0;
// k ++, 是因为nth_element 需要用到数组中最后一个数的位置的下一个位置
nth_element(c, c + k / 2, c + k); // 会将数组中第2个参数位置的数放到正确的位置
LL res = 0;
for (int i = 0; i < k; i ++ )
res += abs(c[i] - c[k / 2]);
cout << res << endl;
return 0;
}
125. 耍杂技的牛(2月4日)
分析
w_i : 重量, s_i:强壮值,
第i头牛的危险值 :
w
1
+
w
2
+
.
.
.
+
w
i
−
1
−
s
i
w_1 + w_2 + ... + w_{i - 1} - s_i
w1+w2+...+wi−1−si
求最大危险值最小
看似二分, 实则贪心
w
i
,
s
i
w
i
+
1
,
s
i
+
1
w_i, s_i\\ w_{i + 1}, s_{i + 1}
wi,siwi+1,si+1
如果
w
i
+
1
+
s
i
+
1
<
w
i
+
s
i
w_{i + 1} + s_{i + 1} < w_i + s_{i}
wi+1+si+1<wi+si, 如果交换这两头牛, 那么危险系数的最大值不会变大
证明了这点后, 任意的方案, 如果存在逆序, 可以交换逆序使得答案不会变差, 经过若干次交换, 必然可以让所有的牛按照两数之和, 从小到大排序, 而且转换完的最大危险值不会比原方案更差
对于任意方案, 以上都是成立的, 那么对于任意的最优方案, 也可以变成从小到大的排序 , 结果不会变差, 那么说明当前方案是一个最优解
对于一个最优解, 能够变成一个当前解, 当前解的结果不会变差, 那么当前解也是一个最优解
所以所有牛从小到大排序, 就是最优解
如图, 第1个小于, 是因为 交换前i + 1部分多了
w
i
w_i
wi
第2个小于是因为
w
i
+
1
+
s
i
+
1
<
w
i
+
s
i
w_{i + 1} + s_{i + 1} < w_{i} + s_{i}
wi+1+si+1<wi+si
总结:
如果
w
i
+
1
+
s
i
+
1
<
w
i
+
s
i
w_{i + 1} + s_{i + 1} < w_{i} + s_{i}
wi+1+si+1<wi+si, 即逆序的话,
所以交换后的危险值的最大值, 小于交换前的, 交换以下两项, 危险值最大值能够减小
code
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 500010;
typedef pair<int, int> PII;
#define x first
#define y second
PII a[N];
int n;
int main(){
cin >> n;
for (int i = 0; i < n; i ++ ){
int x, y;
scanf("%d%d", &x, &y);
a[i] = {x + y, x};
}
sort(a, a + n);
// 要求最大危险值最小, 已经排序了, 表示当前方案最大危险值已经是最小值了, 所以求出最大值即可
int res = -1e9;
for (int i = 0, sum = 0; i < n; i ++ ){
int w = a[i].y, s = a[i].x - w; // w表示重量, s表示强壮值
res = max(res, sum - s);
sum += w;
}
cout << res << endl;
return 0;
}
499. 聪明的质监员(2月5日)
分析
对于每个W, 都可以算出来一个Y, 求Y与给定得S最小差值是多少
可以发现当W越大的时候, 那么
Y
i
Y_i
Yi就会越小, 所以Y就会越小
可以发现Y的图像是单调的, 因此就可以通过二分, 求出来与S最接近的值
即从图像中求得, 满足
Y
≥
S
Y \geq S
Y≥S的W, 在曲线上最靠右的一个W
然后将这个W + 1, 看下两边哪个差最小
题目中W参数类型没有限制, 即可以是整数, 也可以是实数
但可以发现W取小数没有意义, 因为W取小数的话, 题目中考虑
w
j
w_j
wj>= W
实际上就是取
w
j
≥
⌈
W
⌉
w_j \geq \lceil W \rceil
wj≥⌈W⌉的整数, 所以小数没意义
因此只需要考虑W的所有整数值
1.二分
2.二分完后, 需要知道如何快速的求出Y (前缀和)
code
#include <iostream>
using namespace std;
const int N = 2000010;
typedef long long LL;
int L[N], R[N];
LL sum[N];
LL S;
int w[N], v[N], cnt[N];
int n, m;
LL get_y(int W){
for (int i = 1; i <= n; i ++ ){
if (w[i] >= W){
sum[i] = sum[i - 1] + v[i];
cnt[i] = cnt[i - 1] + 1;
}else {
sum[i] = sum[i - 1];
cnt[i] = cnt[i - 1];
}
}
LL res = 0;
for (int i = 0; i < m; i ++ )
res += (sum[R[i]] - sum[L[i] - 1]) * (cnt[R[i]] - cnt[L[i] - 1]);
return res;
}
int main(){
scanf("%d%d%lld", &n, &m, &S);
for (int i = 1; i <= n; i ++ ) scanf("%d%d", &w[i], &v[i]);
for (int i = 0; i < m; i ++ ) scanf("%d%d", &L[i], &R[i]);
int l = 0, r = 1e6 + 1;
while (l < r){
int mid = l + r + 1 >> 1;
if (get_y(mid) >= S) l = mid; // 二分求的是最靠下的W使得 Y >= S
else r = mid - 1;
}
printf("%lld\n", min(get_y(r) - S, S - get_y(r + 1)));
return 0;
}
503. 借教室(2月6日)
分析
问从前往后看, 第1个不能满足的订单是哪个订单, 输出这个订单
如果所有订单都不能满足的话, 输出0
从前往后去满足的, 所以是具有二段性的, 必然存在第1个订单不能满足
当前这个不能满足的, 后面这个就不会去看了
所以是具有二段性的性质, 二分可以出来分界点
所以这题的第一步就是先要想到二分
二分完后, 还要快速判断, 还需要判断下前面若干个订单能不能满足
我们需要看下订单的操作, **每次订单的操作都是将区间内的所有数 - 一个数/ + 一个数 **----> (差分)
然后最终去判断区间内某个数是否小于0
可以用线段树来做, 但数据范围比较大, 会超时
所以这题可以用差分来做, 然后扫描一下区间内是否有小于0的数, 如果小于0, 说明这个订单不能被不满足
如果都可以满足, 说明这个订单可以被满足
举个例子:
如果当前点不能满足的话, 说明第1次不能被满足的订单在左半边
可以满足的话, 说明答案在右半边
code
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
typedef long long LL;
int r[N];
int d[N], s[N], t[N];
LL b[N];
int n, m;
bool check(int mid){
for (int i = 1; i <= n; i ++ ) b[i] = r[i] - r[i - 1]; // 注意差分的写法
for (int i = 1; i <= mid; i ++ ){ // 这里需要循环到mid 表示对mid前面的订单做差分
b[s[i]] -= d[i];
b[t[i] + 1] += d[i];
}
for (int i = 1; i <= n; i ++ ){
// i <= mid是错误的, 因为区间有长度, 可能mid + 1 这段区间是被覆盖的, <0, 因此需要循环到n
b[i] += b[i - 1];
if (b[i] < 0) return true;
}
return false;
}
int main(){
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &r[i]);
for (int i = 1; i <= m; i ++ ) scanf("%d%d%d", &d[i], &s[i], &t[i]);
int l = 1, r = n;
if (!check(m)){ // !check(m) 表示m天都能满足
puts("0");
return 0;
}
while (l < r){
int mid = l + r >> 1;
if (check(mid)) r = mid; // check(mid) 表示mid不能满足, 那么第1个不能满足的在左边
else l = mid + 1;
}
if (check(r)){
printf("-1\n%d\n", r);
return 0;
}
return 0;
}
257. 关押罪犯(2月7日)
分析
有n个监狱, 需要将囚犯放到监狱中, 使得同一个监狱中囚犯之间恩怨的最大值最小
恩怨值[0,
1
0
9
10^9
109]
如果答案是mid(同一个监狱边权的最大值), 换句话来说, 边权>mid的所有边, 可以分配在两个监狱之间, 不让它出现在监狱内部
因为答案最大是mid, mid + 1, …, 不能分配在同一个监狱里
换句话来说, 将>mid的边拿出来之后, 图是一个二分图(所有边都出现在集合之间, 集合内部没有边)
所以这就是一个性质: >mid的所有边会构成一个二分图
这样对每个数都可以判断一下是否满足这个性质, 如果一个数满足这个性质的话, 就表示大于这个数的所有边能构成二分图
考虑下, mid右边的数是否都满足这个性质, 是可以的, **因为相当于去掉了一些边 **, 本来就是一个二分图, 去掉了一些边, 仍然是二分图
因此x右边的数一定都满足这个性质
所以x左边的数一定都不满足这个性质, 假如 x x x左边的某个数 x ′ x' x′满足这个性质, 那么我们的仇恨值可以取 x ′ x' x′, 比x更小, 就矛盾了
所以x左边的数都不满足这个性质
因此这个性质具有二段性
具有二段性的话, 就可以通过二分的方式, 找到分界点
二分后, 判断下由边权> mid的所有边是否能构成二分图
code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 20010, M = 200010;
int h[N], e[M], w[M], ne[M], idx;
int n, m;
int color[N];
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
bool dfs(int u, int c, int mid){
color[u] = c;
for (int i = h[u]; ~i; i = ne[i]){
if (w[i] <= mid) continue;
int j = e[i];
if (color[j]){ // 先判断有无染过
if (color[j] == color[u]) return false;
}else if (!dfs(j, 3 - c, mid)) return false;
}
return true;
}
bool check(int mid){
memset(color, 0, sizeof color);
for (int i = 1; i <= n; i ++ ){
if (!color[i]) // 先判断, 只有没有被染过颜色, 才能进行后一步dfs染色
if (!dfs(i, 1, mid)) return false;
}
return true;
}
int main(){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- ){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
int l = 0, r = 1e9;
while (l < r){
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
cout << r << endl;
return 0;
}