寒假每日一题(提高组)Week 2

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 sisj是k的倍数

举个例子:
如果区间长度是1, 那是就是问 s i − s i − 1 ≡ 0 ( m o d    k ) s_i - s_{i - 1} \equiv 0(\mod k) sisi10(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. sisi10sisi20...sis00
换句话说, s 0 , s 1 , . . . s i − 1 s_0, s_1, ... s_{i - 1} s0,s1,...si1有多少个数与 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 xiy, 能不能取到 y y y还不知道

环中不可能存在所有 x i > 0 x_i > 0 xi>0, 如果存在这种情况, 可以让所有 x i − 1 x_i - 1 xi1, 可以使得糖果均分, 并且 ∑ ∣ 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 xi0,xi<0, 必然存在一个位置, 使得左边 x i ≥ 0 x_i \geq 0 xi0, 右边 x i < 0 x_i < 0 xi<0
在这里插入图片描述
意味着这个小朋友会给左右两边糖果
在这里插入图片描述
从这个地方断开, 那么第1个和最后一个小朋友是图中圈出来的同一个小朋友, 既会往左边给, 又会往右边给

来构造一种方案, 使得刚才上述所说的最小值方案能够构造出来

从前往后依次看 x k x_k xk,

  1. x k ≥ 0 x_k \geq 0 xk0 立即给
  2. 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 xk10

(因为 a k − 1 a_{k - 1} ak1 a k a_k ak的左半边, 上面已经证明了前面的小朋友一定是会给后面的小朋友糖果, 即 x 1 , . . x k − 1 > 0 x_1, .. x_{k - 1} > 0 x1,..xk1>0)

那么说明在操作 x k − 1 x_{k - 1} xk1之前 a 1 + . . . + a k − 1 > ( k − 1 ) b a_1 + ... + a_{k - 1} > (k - 1)b a1+...+ak1>(k1)b, 那么操作完之后 a 1 + . . . + a k − 1 = ( k − 1 ) b a_1 + ... + a_{k - 1} = (k - 1)b a1+...+ak1=(k1)b

yxc:老师上课说是 a 1 + . . . + a k − 1 ≤ ( k − 1 ) b a_1 + ... + a_{k - 1} \leq (k - 1)b a1+...+ak1(k1)b 有点不理解???
按理说已经操作完成了, 那么就表示可以k - 1个小朋友已经到达平均值了

然后分析 x k ≥ 0 x_k \geq 0 xk0, 那么说明前k个小朋友数量比平均值多 a 1 + . . . + a k − 1 + a k ≥ k b a_1 + ... + a_{k - 1} + a_{k} \geq kb a1+...+ak1+akkb

yxc老师上课这里讲的是 a 1 + . . . + a k − 1 + a k = k b a_1 + ... + a_{k - 1} + a_{k} = kb a1+...+ak1+ak=kb ???

看看现在有的不等式关系
a 1 + . . . + a k − 1 = ( k − 1 ) b a_1 + ... + a_{k - 1} = (k - 1)b a1+...+ak1=(k1)b
a 1 + . . . + a k − 1 + a k ≥ k b + x k a_1 + ... + a_{k - 1} + a_{k} \geq kb + x_k a1+...+ak1+akkb+xk
可以得到 a k ≥ b + x k a_k \geq b + x_k akb+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+xkxk

因此就证明了方案的合法性, 并且可以取到最小值

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+...+wi1si

求最大危险值最小
看似二分, 实则贪心

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 YS的W, 在曲线上最靠右的一个W
然后将这个W + 1, 看下两边哪个差最小
在这里插入图片描述
题目中W参数类型没有限制, 即可以是整数, 也可以是实数
但可以发现W取小数没有意义, 因为W取小数的话, 题目中考虑 w j w_j wj>= W
实际上就是取 w j ≥ ⌈ W ⌉ w_j \geq \lceil W \rceil wjW的整数, 所以小数没意义
因此只需要考虑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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值