中国石油大学校赛总结

传送门
原题解

C - 小菲爱数数

题目描述

小菲刚刚学完质数,所以她很喜欢计算一个整数有多少个质因子.每当小菲看到一些整数,她都会很快计算出这些数字的最小公倍数,然后数出最小公倍数有多少个质因子.

但是大菲觉得这太简单了,她决定考验一下小菲,让小菲计算出一个数组所有子数组最小公倍数的质因子数目之和.

小菲觉得这个问题太难了,所以她想问问你.

正式地讲,给出一个长度为n的数组 a 1 a_1 a1, a 2 a_2 a2, a 3 a_3 a3,⋯, a n a_n an,请计算 ∑ i = 1 n ∑ j = i n f ( l c m ( a i ∼ j ) ) \sum\limits_{i=1}^n\sum\limits_{j=i}^nf(lcm(a_{i\sim j})) i=1nj=inf(lcm(aij)),其中f(x)表示x的质因子数目,lcm(b)表示数组b中所有元素的最小公倍数, a l ∼ r a_{l\sim r} alr表示a中从l到r连续的一段子数组,即 a l a_l al, a l + 1 a_{l+1} al+1, a l + 2 a_{l+2} al+2,⋯, a r a_r ar

原数组中连续的一段区间内的元素组成的数组被称为原数组的子数组.例如[2],[2,3],[1,2,3,4]是[1,2,3,4]的子数组,但是[1,2,4]不是,因为不是连续的一段.

输入描述:

本题有多组测试数据.

第一行包含一行一个正整数 T ( 1 ≤ T ≤ 1 0 4 ) T(1\leq T\leq10^4) T(1T104)表示测试数据的数目,然后输入T组独立的数据.

每组数据第一行输入一行一个正整数 n ( 1 ≤ n ≤ 2 × 1 0 5 ) n(1\leq n\leq2 \times10^5) n(1n2×105),表示数组的长度.

每组数据第二行输入以空格分隔的n个正整数,表示输入的数组 a n ( 1 ≤ a i ≤ 1 0 6 , 1 ≤ i ≤ n ) a_n(1\leq a_i\leq10^6,1\leq i\leq n) an(1ai106,1in).

保证所有测试数据的n之和不超过 2 × 1 0 5 2 \times10^5 2×105

输出描述:

对于每个测试样例,输出一行一个非负整数表示答案.

tag

思维

思路

a 1 ∼ n a_{1\sim n} a1n分解质因子,考虑计算每个质因子对答案的贡献.

不妨设质因子 p p p出现的位置有 b 1 , b 2 , b 3 , ⋯   , b k b_1,b_2,b_3,\cdots ,b_k b1,b2,b3,,bk,对于任意一个区间只要包含任意的 b i b_i bi就会对答案产生 1 1 1的贡献.

正难则反,我们可以计算有多少个区间不包含任何 b i b_i bi,然后用总区间数减去不包含任何 b i b_i bi的区间数得到质因子对答案的贡献.

显然只有处于任意两个相邻 b b b之间的区间是不包含任何 b i b_i bi的.

对于每个质因子我们先给答案加上 n ( n + 1 ) 2 \frac{n(n+1)}{2} 2n(n+1),然后对于所有的相邻 b b b之间的区间,假设长度为 l e n len len,我们让答案减去 l e n ( l e n + 1 ) 2 \frac{len(len + 1)}{2} 2len(len+1).

当然,你可能需要通过加一些哨兵或者特判来解决边界问题.

根据分解质因子的方法不同,时间复杂度为 O ( n A ) O(n\sqrt{A}) O(nA )或者 O ( n log ⁡ A ) O(n\log{A}) O(nlogA).

因为含有多组测试样例,所以你需要用类似map或dict之类的关联容器来存储每个质因子出现的位置,以保证你枚举质因子的时间复杂度是 O ( n log ⁡ A ) O(n\log{A}) O(nlogA).

代码

#include<iostream>
#include<cstring>
#include<map>
#include<vector>
using namespace std;
using LL = long long;
int main(){
    cin.tie(0);
    cout.tie(0);
    ios::sync_with_stdio(0);
    auto g = [](int x){
        return 1LL * x * (x + 1) / 2;
    };
    int T;
    cin >> T;
    while(T--){
        int n;
        cin >> n;
        map<int, vector<int> > mp;
        for(int i = 1; i <= n; i++){
            int x;
            cin >> x;
            for(int j = 2; j * j <= x; j++){
                if (x % j == 0){
                    mp[j].push_back(i);
                    while(x % j == 0) x /= j;
                }
            }
            if (x > 1) mp[x].push_back(i);
        }
        LL ans = 0;
        for(auto &[x, v] : mp){
            ans += g(n);
            v.push_back(n + 1);
            int last = 0;
            for(auto u : v){
                ans -= g(u - last - 1);
                last = u;
            }
        }
        cout << ans << '\n';
    }
}

E - Construction Complete!

题目描述

指挥官想要在战场上建造一座新的生产建筑.

整个战场可以视为一个n行m列的二维网格,有一些网格中已经建造了一些生产建筑,还有一些网格是矿区,斜坡,悬崖等无法建造建筑的地方,剩下的位置是可以建造建筑的空地.

现在指挥官想建造一座新的占据r行c列网格的矩形建筑,但是必须要满足以下要求.

  1. 建筑占据的所有网格都必须为空地.
  2. 新的建筑必须在已有的建筑基础上延伸建造,即新建筑与距离其最近的已有建筑之间距离不能超过d.把第x行,第y列的网格的表示为(x,y),则(x1,y1)与(x2,y2)之间的距离为 ∣ x 1 − x 2 ∣ + ∣ y 1 − y 2 ∣ \left|x1-x2\right|+\left|y1-y2\right| x1x2+y1y2.

新矩形建筑与(x,y)之间的距离为矩形建筑占据的所有网格与(x,y)之间的距离中的最小值.

你是指挥官的顾问,现在指挥官想让你帮忙计算建造一座占据r行c列网格的矩形新建筑的方案数量.

对于两种方案,如果矩形新建筑最左上角的网格位置是不同的,则我们认为这两种建造方案是不同的.

输入描述:

本题含有多组测试数据,

第1行包含一行一个正整数T(1≤T≤ 1 0 5 10^5 105),表示测试数据的数目,然后输入T组独立的数据.

每组数据第1行输入一行五个以空格分隔的正整数n,m,r,c,d,分别表示战场的行数和列数,新建筑占据的行数和列数,以及已有建筑能延伸的最大距离.

数据范围满足2≤n⋅m≤ 1 0 6 10^6 106 , 1≤r≤n , 1≤c≤m,1≤d≤n+m−2.

接下来输入n行,每行m个字符表示战场的情况.第i行的第j个字符代表坐标为(i,j)的方格情况,如果字符是’x’,则表示该位置是已有的生产建筑,如果字符是’#‘则表示该位置是矿区,斜坡,悬崖等无法建造建筑的地方,如果字符是’.',则表示该位置是空地.

保证所有测试数据的n⋅m之和不超过 1 0 6 10^6 106.

输出描述:

对于每组测试数据输出一行一个非负整数表示建造方案的数量.

tag

前缀和,BFS

思路

首先以所有的x为起点,用广度优先搜索(BFS)求取多源最短路 d i s t dist dist.

对于任意一个矩形,首先要满足矩形内所有点都是空地,其次要满足矩形内存在一个 ( x , y ) (x, y) (x,y)点满足 d i s t x , y ≤ d dist_{x,y} \le d distx,yd.

对于第一个条件,我们令所有空地的位置为 1 1 1,其他位置为 0 0 0,然后处理出二维前缀和.查询时只需判断矩形和是否等于矩形面积即可.

对于第二个条件,我们令所有 d i s t x , y ≤ d dist_{x,y} \le d distx,yd的点 ( x , y ) (x, y) (x,y) 1 1 1,其他位置为 0 0 0,同样处理出二维前缀和,查询时只需判断矩形和是否大于 0 0 0即可.

然后就可以直接枚举矩形左上角的位置利用处理出的二维前缀和 O ( 1 ) O(1) O(1)判断是否合法.时间复杂度和空间复杂度均为 O ( ∑ n m ) O(\sum{nm}) O(nm).

代码

#include<iostream>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
using LL = long long;
int main(){
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(0);
	const int dx[4] = {-1, 0, 1, 0};
	const int dy[4] = {0, -1, 0, 1};
	int T;
	cin >> T;
	while(T--){
		int n, m, r, c, dist;
		cin >> n >> m >> r >> c >> dist;
		vector<string> g(n + 1);
		for(int i = 1; i <= n; i++){
			g[i].resize(m + 1);
			for(int j = 1; j <= m; j++)
				cin >> g[i][j];
		}
		vector<vector<int> > d(n + 1, vector<int>(m + 1, n + m));
		queue<pair<int, int> > q;
		for(int i = 1; i <= n; i++)
			for(int j = 1; j <= m; j++)
				if (g[i][j] == 'x'){
					d[i][j] = 0;
					q.push({i, j});
				}
		while(!q.empty()){
			auto [x, y] = q.front(); q.pop();
			for(int u = 0; u < 4; u++){
				int nx = x + dx[u], ny = y + dy[u];
				if (nx < 1 || nx > n || ny < 1 || ny > m) continue;
				if (d[nx][ny] < n + m) continue;
				d[nx][ny] = d[x][y] + 1;
				q.push({nx, ny});
			}
		}
		vector<vector<int> > s1(n + 1, vector<int>(m + 1, 0)), s2(n + 1, vector<int>(m + 1, 0));
		for(int i = 1; i <= n; i++)
			for(int j = 1; j <= m; j++){
				s1[i][j] = (d[i][j] <= dist) + s1[i - 1][j] + s1[i][j - 1] - s1[i - 1][j - 1];
				s2[i][j] = (g[i][j] == '.') + s2[i - 1][j] + s2[i][j - 1] - s2[i - 1][j - 1];
			}
		auto query = [&](const vector<vector<int> > &s, int x1, int y1, int x2, int y2){
			return s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];
		};
		int ans = 0;
		for(int i = 1; i + r - 1 <= n; i++)
			for(int j = 1; j + c - 1 <= m; j++)
				if (query(s1, i, j, i + r - 1, j + c - 1) > 0 && query(s2, i, j, i + r - 1, j + c - 1) == r * c)
					ans++;
		cout << ans << '\n';
	}
}

F - 最小异或对

题目描述

给出一个多重集合(元素可以重复的集合),你需要提供以下操作:

  1. ADD x,向多重集合里添加一个元素x,多重集合内元素可以重复
  2. DEL x,从多重集合中删除一个元素x,保证要删除的元素一定存在,如果存在多个x则仅删除其中任意1个
  3. QUE,查询集合中的最小异或对的值,即找到集合中任何两个元素(可以相等)异或能得到的最小值,保证询问时集合包含的元素数量不少于2个

对于每个QUE操作,你需要输出查询的结果.

以上操作中涉及的操作数x均为非负整数.

输入描述:

第一行输入操作的数量n(1≤n≤2× 1 0 5 10^5 105),以下n行每行表示一个操作,操作的格式见题面.对于非负整数x满足 0 ≤ x < 2 30 0≤x<2^{30} 0x<230.

输出描述:

对于每个QUE操作,输出一行一个非负整数,表示询问的答案.

tag

平衡树/01字典树

思路一

首先有一个结论,最小异或对一定是集合排序后相邻的数.

我们只需要证明对于任意的 a < b < c a < b < c a<b<c,都满足最小值一定是 a ⊕ b a\oplus b ab或者 b ⊕ c b \oplus c bc,而不可能是 a ⊕ c a \oplus c ac.

满足这个条件后,如果我们选取的数在排序后的集合中不相邻,通过选相邻的数一定能使答案更小.

下面我们证明这个结论.

对于 a ⊕ c a\oplus c ac,找到其二进制表示中最高的为 1 1 1的位为第 x x x位,则 a a a c c c高于 x x x的位一定相同,第 x x x位一定不同,因为 a < c a < c a<c,一定是 a a a x x x位为 0 0 0, c c c x x x位为 1 1 1.

因为 a < b < c a < b < c a<b<c,高于 x x x的位 b b b一定也和 a , c a,c a,c都相同, b b b的第 x x x位可能为 0 0 0或者为 1 1 1.

所以 a ⊕ b a\oplus b ab b ⊕ c b \oplus c bc中一定有一个数第 x x x位为 0 0 0,而 a ⊕ c a\oplus c ac x x x位为 1 1 1,即一定有一个数是小于 a ⊕ c a \oplus c ac的,由此结论得证.

所以我们需要维护原集合的有序序列和有序序列中所有相邻数字的异或值.

每次加入一个数 x x x时,找到有序集合中 x x x的前驱 p r e pre pre和后继 n x t nxt nxt,在维护异或值的数据结构中删除 n x t ⊕ p r e nxt\oplus pre nxtpre,加入 x ⊕ p r e x \oplus pre xpre x ⊕ n x t x \oplus nxt xnxt.

删除一个数 x x x时类似,删除 x x x后同样找到 x x x的前驱 p r e pre pre和后继 n x t nxt nxt,加入 n x t ⊕ p r e nxt\oplus pre nxtpre,删除 x ⊕ p r e x \oplus pre xpre x ⊕ n x t x \oplus nxt xnxt即可.

每次查询输出所有相邻数字的异或值的最小值即可.

这要求我们需要快速找到有序序列中一个数字的前驱和后继,可以使用平衡树来维护.

用平衡树数维护原序列,然后使用一个支持增删和查询最小值的数据结构维护所有所有相邻数的异或值,同样可以使用平衡树.

当然你需要通过特判处理一些前驱或者后继不存在的情况.

增删查的时间复杂度均为 O ( log ⁡ n ) O(\log{n}) O(logn)总时间复杂度为 O ( n log ⁡ n ) O(n\log{n}) O(nlogn),空间复杂度为 O ( n ) O(n) O(n).

思路二

如果你不知道以上的结论,有另一种01Trie的做法.

从高位到低位建立01Trie,每个结点都维护 3 3 3个信息:

  1. 当前子树内能凑出的最小异或对的答案
  2. 子树内的元素个数
  3. 如果子树内只有一个元素,记录这个元素的值.

首先对于叶子结点,如果包含的元素数量不大于 1 1 1,那么当前答案为无穷大,否则答案为 0 0 0

在01Trie上,除了叶子结点外每个结点都有两个儿子.那么对于当前结点选取两个元素有 3 3 3种可能,在左儿子中选取两个元素,在右儿子中选取两个元素,或者左右儿子各选一个元素.

如果能在同一个儿子里选择两个元素答案一定好于在两个儿子中各选一个元素,所以可以用儿子的答案更新当前结点.

如果两个儿子结点包含的元素数量都不大于 1 1 1,那么我们只能在两个儿子中各选一个元素然后更新答案.

当然如果当前子树中包含的元素数量不大于 1 1 1,答案为无穷大.

添加和删除时使用类似线段树的方式从下往上更新答案,每次查询时只需输出根节点的答案即可.

可以发现这里的01Trie完全等价于动态开点的权值线段树.

每次插入和删除的复杂度为01Trie的高度,查询的复杂度为 O ( 1 ) O(1) O(1),总的时间复杂度为 O ( n log ⁡ A ) O(n\log{A}) O(nlogA),空间复杂度也为 O ( n log ⁡ A ) O(n\log{A}) O(nlogA).

代码一

#include<iostream>
#include<cstring>
#include<set>
using namespace std;
using LL = long long;
int main(){
    cin.tie(0);
    cout.tie(0);
    ios::sync_with_stdio(0);
    multiset<int> s, v;
    int n;
    cin >> n;
    while(n--){
        string op;
        cin >> op;
        if (op[0] == 'A'){
            int x;
            cin >> x;
            auto it = s.lower_bound(x);
            if (it != s.end())
                v.insert(*it ^ x);
            if (it != s.begin()){
                v.insert(*prev(it) ^ x);
            }
            if (it != s.begin() && it != s.end()){
                v.erase(v.lower_bound(*it ^ *prev(it)));
            }
            s.insert(x);
        }
        else if (op[0] == 'D'){
            int x;
            cin >> x;
            s.erase(s.lower_bound(x));
            auto it = s.lower_bound(x);
            if (it != s.end())
                v.erase(v.lower_bound(*it ^ x));
            if (it != s.begin()){
                v.erase(v.lower_bound(*prev(it) ^ x));
            }
            if (it != s.begin() && it != s.end()){
                v.insert(*it ^ *prev(it));
            }
        }
        else cout << *v.begin() << '\n';
    }
}

代码二

#include<iostream>
#include<cstring>
#include<vector>
#include<numeric>
#include<functional>
using namespace std;
using LL = long long;
const int maxn = 2e5 + 5, INF = 1 << 30;
struct Info {
    int dp, cnt, val;
    Info() : dp(INF), cnt(0), val(-1) {} 
    Info(int x) : dp(INF), cnt(1), val(x) {} 
    Info(int dp, int cnt, int val) : dp(dp), cnt(cnt), val(val) {}
}tr[maxn * 32];
int son[maxn * 32][2];
int root, idx;
Info operator+(const Info &a, const Info &b){
    Info ret = Info();
    ret.cnt = a.cnt + b.cnt;
    ret.dp = min(a.dp, b.dp);
    if (a.cnt == 1 && b.cnt == 1) ret.dp = a.val ^ b.val;
    if (ret.cnt == 1) ret.val = a.cnt ? a.val : b.val;
    return ret;
}
void insert(int &u, int x, int dep){
    if (!u) u = ++idx;
    if (dep == -1){
        ++tr[u].cnt;
        if (tr[u].cnt == 1) tr[u] = Info(x);
        else if (tr[u].cnt == 2) tr[u].dp = 0;
        return;
    }
    int bit = (x >> dep & 1);
    insert(son[u][bit], x, dep - 1);
    tr[u] = tr[son[u][0]] + tr[son[u][1]];
}
void del(int &u, int x, int dep){
    if (dep == -1){
        --tr[u].cnt;
        if (tr[u].cnt == 0) tr[u] = Info();
        else if (tr[u].cnt == 1) tr[u] = Info(x);
        return;
    }
    int bit = (x >> dep & 1);
    del(son[u][bit], x, dep - 1);
    tr[u] = tr[son[u][0]] + tr[son[u][1]];
}
int main(){
    cin.tie(0);
    cout.tie(0);
    ios::sync_with_stdio(0);
    int n;
    cin >> n;
    int root = ++idx;
    while(n--){
        string op;
        cin >> op;
        if (op[0] == 'A'){
            int x;
            cin >> x;
            insert(root, x, 29);
        }
        else if (op[0] == 'D'){
            int x;
            cin >> x;
            del(root, x, 29);
        }
        else cout << tr[root].dp << '\n';
    }
}

G - 魔术卡片

题目描述

据说魔术卡片可以猜出你心中在想什么.

一组魔术卡片中每张卡片的大小和材料都是相同的,每张卡片上都有52个方格,其中每个方格都固定地对应某张扑克牌(不同的卡片上相同位置的方格对应的扑克牌是一样的),每张卡片上都会有一些扑克牌出现在这张牌对应的格子里(也有可能一些格子是空的).
在这里插入图片描述

因为卡片是半透明的,所以如果把一些卡片重叠到一起,我们就能看到所有在这些卡片中出现过的扑克牌.

魔术的表演方法是让观众心中想一张扑克牌,然后给他看这组魔术卡片,让观众描述每张卡片上是否有他心中所想的牌。接着表演者就可以准确地告知观众他心中想的是哪张扑克牌。

这是因为这组卡片有神奇的性质,如果把所有的不含有观众心中所想的数字的卡片叠在一起,那么只会有一张牌的位置上是空的,而这张牌就是观众心中所想的牌.

如图,把所有不含观众所想数字的魔术卡片重叠后,只有梅花8的位置是空的,所以观众心中想的牌是梅花8.
在这里插入图片描述

为了简化问题,我们现在让观众想一个1∼n的正整数,你需要设计一组卡片,能利用上文中提到的方法猜出观众心中想的数字,此外为了方便携带,卡片的数量不应该超过8张.

具体来讲就是你需要设计出一组不超过8张的卡片,每张卡片上都包含一些数字,要求对于任意的1≤i≤n都满足:假设所有不包含i的卡片上出现过的数字组成的集合为s,那么s包含1∼n中除了i以外的所有数字.

输入描述:

本题含有多组测试数据.

第1行包含一行一个正整数T(1≤T≤59),表示测试数据的数目,然后输入T组独立的数据.

每组数据输入一行一个正整数n(2≤n≤60),表示可能的观众心中所想数字的最大值.

输出描述:

对于每组测试样例数据,

第1行输出一个正整数m(1≤m≤8),表示卡片的数量.

第2∼m+1行每行输出一张卡片的内容,首先输出一个非负整数s(0≤s≤n)代表这张卡片上出现了多少个数字,然后
输出卡片上出现的s个正整数(1≤ x i x_i xi≤n),每行数字之间以空格分隔.

如果存在多种可能的答案,输出其中任意一种都是正确的.

tag

构造

思路

对于 1 ∼ n 1 \sim n 1n我们都用一个 8 8 8位的二进制数表示其状态,第 i i i位取 0 / 1 0/1 0/1表示第 i i i张卡片上是否有这个数字,然后我们考虑这些数之间需要具有怎样的性质.

对于任意数字 x x x,假设其没有出现在第 c 1 , c 2 , c 3 , ⋯ c_1, c_2, c_3,\cdots c1,c2,c3,张卡片上,那么除了 x x x以外的所有数字都至少在第 c 1 , c 2 , c 3 , ⋯ c_1,c_2,c_3,\cdots c1,c2,c3,上出现过一次.即第 c 1 , c 2 , c 3 , ⋯ c_1,c_2,c_3,\cdots c1,c2,c3,上至少有一位为 1 1 1.所以所有除了 x x x以外的数字的二进制表示一定不是 x x x的子集.

推广到所有数字上,一定没有一个数字的二进制表示是另一个数字二进制表示的子集.所以我们只要找出一些二进制数,满足没有一个数是其他任何数的子集即可.

一种简单的构造方法是在所有 8 8 8位的二进制数字里选取二进制表示中 1 1 1的数量为 4 4 4的数来构造,这些数显然不会有一个数是另一个数的子集.并且这样的数有 C 8 4 = 70 C_8^4 = 70 C84=70个,而 n ≤ 60 n \le 60 n60.

代码

#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
using LL = long long;
int main(){
    cin.tie(0);
    cout.tie(0);
    ios::sync_with_stdio(0);
    int T;
    cin >> T;
    while(T--){
        const int N = 8;
        int n;
        cin >> n;
        int cnt = 0;
        vector<vector<int> > ans(N);
        for(int i = 0; i < 1 << N; i++){
            if (__builtin_popcount(i) != 4) continue;
            if (++cnt > n) break;
            for(int j = 0; j < N; j++)
                if (i >> j & 1) ans[j].push_back(cnt);
        }
        cout << N << '\n';
        for(int i = 0; i < N; i++){
            cout << ans[i].size();
            for(auto x : ans[i]) cout << ' ' << x;
            cout << '\n';
        }
    }
}

H - 推队

题目描述

小菲最近在玩一款回合制精灵对战游戏,每只精灵都有体力,攻击,防御和速度四种属性,小菲的精灵初始的体力,攻击,防御和速度属性分别为 H 0 H_0 H0, A 0 A_0 A0, D 0 D_0 D0, S 0 S_0 S0,初始的所有属性都为0级.

小菲她正准备用一只精灵击败对方的所有n只精灵,对方第i只精灵的体力,攻击,防御,速度属性分别为 h i h_i hi, a i a_i ai, d i d_i di, s i s_i si.
在对战中小菲的精灵可以使用攻击技能或者属性技能,但是对方的精灵只能使用攻击技能.

使用攻击技能的效果可以对对方造成一定的伤害,让对方体力减少.假设攻击方的攻击为 a a t t a c k e r a_{attacker} aattacker​,被攻击方的防御为 d d e f e n d e r d_{defender} ddefender,则攻击方的攻击技能会让被攻击方体力减少 m a x ( a a t t a c k e r − d d e f e n d e r , 0 ) max(a_{attacker}−d_{defender},0) max(aattackerddefender,0).攻击技能没有次数限制可以无限次使用.

使用属性技能的效果是让己方精灵的攻击,防御,速度其中一项属性提升1级,小菲的精灵在攻击,防御,速度升到i级后相应的属性值分别变为 A i A_i Ai, D i D_i Di, S i S_i Si,每项属性最多升级到k级.

由于游戏有一些神奇的Bug(或者说是Feature),并不保证升级后的属性一定大于升级前的属性.

在一场对战中,她会按顺序挑战对方的n只精灵,一开始小菲的精灵和对方的第1只精灵同时登场并开始一个回合.
每一回合会按次序进行以下步骤

  1. 比较双方精灵的速度决定哪方先出手,速度快的精灵先出手,如果双方速度相同则小菲的精灵先出手.
  2. 先出手的精灵释放技能,技能的效果生效.
  3. 后出手的精灵释放技能,技能的效果生效.

以上步骤中如果某次释放技能后导致小菲的精灵体力减少到0或更少,则小菲立即失败;如果释放技能导致对方的第i只精灵体力减少到0或更少,则对方的第i只精灵被击败退场,第i+1只精灵登场,如果第i+1只精灵不存在则小菲立即胜利.如果当前回合结束时没有精灵退场,那么开始下一个新的回合.

注意当对方精灵退场,新精灵登场或者新回合开始时小菲的精灵已损失的体力不会恢复,但已经强化的属性等级会保留.当有精灵退场时,当前回合立即结束,如有新精灵登场,则开始一个新的回合.

大菲帮小菲获得了对手的精灵资料,小菲想请你帮忙判断能否她能否推队(击败对手的所有n只精灵),如果能推队的话,她还想知道小菲的精灵最后最多能剩下多少体力.

输入描述:

本题有多组测试数据.

第1行包含一行一个正整数T(1≤T≤ 1 0 4 10^4 104),表示测试数据的数目,然后输入T组独立的数据.

每组数据第1行输入一行以空格分隔的两个非负整数n,k(1≤n≤ 1 0 4 10^4 104,0≤k≤3),表示对手的精灵数量和属性能强化到的最高等级.

第2行输入一行一个整数 H 0 H_0 H0,表示小菲的精灵的体力值.

接下来三行,每行输入k+1个以空格分隔的正整数,分别表示 A 0 − k , D 0 − k , S 0 − k A_{0-k},D_{0-k},S_{0-k} A0k,D0k,S0k(1≤ H 0 H_0 H0, A i A_i Ai, D i D_i Di, S i S_i Si 1 0 9 10^9 109),代表攻击,防御,速度这三个属性的初始值与升级后对应的值.

最后输入n行,每行输入4个正整数,表示对手第i只精灵的 h i h_i hi, a i a_i ai, d i d_i di, s i s_i si(1≤ h i h_i hi, a i a_i ai, d i d_i di, s i s_i si 1 0 9 10^9 109),即第i只精灵的体力,攻击,防御,速度.

保证所有测试数据的n之和不超过 1 0 4 10^4 104.

输出描述:

对于每组测试数据,输出一行一个整数表示答案.如果小菲不能推队,输出−1, 否则输出小菲的精灵最后击败对方的n只精灵后最多能剩下的体力值.

tag

dp

思路

我们用 d p i , a , b , c dp_{i, a, b, c} dpi,a,b,c表示击退对手 i i i只精灵,攻击等级为 a a a,防御等级为 b b b,速度等级为 c c c时能剩余的最大体力,如果无法达到该状态则为 − 1 -1 1.

考虑状态转移,因为 k k k很小,所以我们可以枚举每种属性当前的等级和该回合准备给每个属性提升的等级进行转移.

关键在于计算某种策略下击败对手第 i i i只精灵最少会损失多少体力.

虽然提升等级可能会导致属性值降低,但是我们可以发现在最优策略中,击败第 i i i只精灵后每项属性一定不会低于对战第 i i i只精灵之前的属性.

因为强化某项属性值但是使得这项属性降低对我们当前的对战是没有好处的,我们完全可以选择该回合强化到更高的属性值或者这回合不强化全部留到与对手下一只精灵对战时强化.这两种策略中一定至少有一种不劣于使某项属性值降低的策略.

所以当我们确定对战第 i i i只精灵时,最优策略的操作顺序一定是先强化防御,然后强化攻击和速度,然后再使用攻击技能.

因为 k k k很小,对于强化过程中受到的伤害我们可以直接暴力模拟,时间复杂度为 O ( k ) O(k) O(k),强化完之后属性值固定,可以 O ( 1 ) O(1) O(1)计算.

总时间复杂度为 O ( ∑ n k 7 ) O(\sum{nk^7}) O(nk7).

代码

#include<iostream>
#include<cstring>
using namespace std;
using LL = long long;
const int maxn = 1e4 + 5;
int f[maxn][4][4][4];
int main(){
    cin.tie(0);
    cout.tie(0);
    ios::sync_with_stdio(0);
    int T;
    cin >> T;
    while(T--){
        int n, k;
        cin >> n >> k;
        int H, A[4], D[4], S[4];
        cin >> H;
        for(int i = 0; i <= k; i++) cin >> A[i];
        for(int i = 0; i <= k; i++) cin >> D[i];
        for(int i = 0; i <= k; i++) cin >> S[i]
        for(int i = 0; i <= n; i++){
            for(int a = 0; a <= k; a++)
            for(int b = 0; b <= k; b++)
            for(int c = 0; c <= k; c++)
                f[i][a][b][c] = -1;
        }
        f[0][0][0][0] = H;
        for(int i = 0; i < n; i++){
            int h, a, d, s;
            cin >> h >> a >> d >> s;
            auto update = [&](int l1, int l2, int l3){
				int cur = f[i][l1][l2][l3];
                auto get = [&](int c1, int c2, int c3){
					int remain = cur;
                    for(int i = 1; i <= c2; i++){
						if (S[l3] >= s){
							remain -= max(0, a - D[l2 + i]);
							if (remain <= 0) return -1;
						}
						else{
							remain -= max(0, a - D[l2 + i - 1]);
							if (remain <= 0) return -1;
						}
					}
					if (A[l1 + c1] <= d) return -1;
					int dec1 = max(0, a - D[l2 + c2]);
					int dec2 = A[l1 + c1] - d;
					if (1LL * (c1 + c3) * dec1 >= remain) return -1;
					if (dec1 == 0) return remain;
					remain -= (c1 + c3) * dec1;
					int cnt = (h + dec2 - 1) / dec2 - (S[l3 + c3] >= s);
					if (1LL * cnt * dec1 >= remain) return -1;
					remain -= cnt * dec1;	
					return remain;
                };
                for(int c1 = 0; c1 <= k - l1; c1++)
                for(int c2 = 0; c2 <= k - l2; c2++)
                for(int c3 = 0; c3 <= k - l3; c3++)
                    f[i + 1][l1 + c1][l2 + c2][l3 + c3] = max(f[i + 1][l1 + c1][l2 + c2][l3 + c3], get(c1, c2, c3));
            };
            for(int a = 0; a <= k; a++)
            for(int b = 0; b <= k; b++)
            for(int c = 0; c <= k; c++)
                if (f[i][a][b][c] > 0)
                    update(a, b, c);
        }
        int ans = -1;
        for(int a = 0; a <= k; a++)
        for(int b = 0; b <= k; b++)
        for(int c = 0; c <= k; c++)
            ans = max(ans, f[n][a][b][c]);
        cout << ans << '\n';
    }
}

I - 最大公约数求和

题目描述

给定非负整数n,k, 请计算 ∑ i = 1 n ( g c d ( i , k ) ⋅ i k ) ( m o d   1000000007 ) \sum\limits_{i=1}^n(gcd(i,k)⋅i^k)(mod\ 1000000007) i=1n(gcd(i,k)ik)(mod 1000000007).

其中x mod m表示x除以m的余数,gcd(a,b)表示a和b的最大公约数.

注意对于任意正整数x,均有gcd(x,0)=x.

输入描述:

输入一行以空格分隔的两个非负整数n,k(1≤n≤2× 1 0 7 10^7 107,0≤k≤ 1 0 18 10^{18} 1018),请注意n的数据范围.

输出描述:

输出一行一个非负整数代表 ∑ i = 1 n ( g c d ( i , k ) ⋅ i k ) ( m o d   1000000007 ) \sum\limits_{i=1}^n(gcd(i,k)⋅i^k)(mod\ 1000000007) i=1n(gcd(i,k)ik)(mod 1000000007)的结果.

tag

积性函数

思路一

f ( x ) = x k f(x) = x^k f(x)=xk g ( x ) = gcd ⁡ ( x , k ) g(x) = \gcd(x, k) g(x)=gcd(x,k)都是积性函数,可以在线性筛的过程中计算.

x x x为质数,直接计算 f ( x ) f(x) f(x) g ( x ) g(x) g(x),计算这两个函数的复杂度都为 O ( log ⁡ 2 k ) O(\log_2{k}) O(log2k).

x x x为合数,在线性筛的过程中, x x x会被其最小质因子筛掉,设其最小质因子为 p p p.

f ( x ) = x k f(x) = x^k f(x)=xk是完全积性函数,因此有 f ( x ) = f ( p ) ⋅ f ( x p ) f(x) = f(p) \cdot f(\frac{x}{p}) f(x)=f(p)f(px).

对于 g ( x ) = gcd ⁡ ( x , k ) g(x) = \gcd(x, k) g(x)=gcd(x,k),只需判断是否有 k g ( x p ) ≡ 0 (   m o d     p ) \frac{k}{g(\frac{x}{p})} \equiv 0 (\bmod \ p) g(px)k0(mod p),如果是则 g ( x ) = p ⋅ g ( x p ) g(x) = p \cdot g(\frac{x}{p}) g(x)=pg(px),否则 g ( x ) = g ( x p ) g(x) = g(\frac{x}{p}) g(x)=g(px).

质数密度 π ( n ) ≈ n ln ⁡ n \pi(n) \approx \frac{n}{\ln{n}} π(n)lnnn,所以总时间复杂度为 O ( log ⁡ 2 k ln ⁡ n ⋅ n + n ) ≈ O ( n ) O(\frac{\log_2{k}}{\ln{n}} \cdot n + n) \approx O(n) O(lnnlog2kn+n)O(n).

思路二

f ( x ) = x k f(x) = x^k f(x)=xk很容易发现是积性函数,但是观察到 g ( x ) = gcd ⁡ ( x , k ) g(x) = \gcd(x, k) g(x)=gcd(x,k)是积性函数还是有一定难度的.

f ( x ) = x k f(x) = x^k f(x)=xk通过解法 1 1 1中的方式计算.

而对于 g ( x ) = gcd ⁡ ( x , k ) g(x) = \gcd(x, k) g(x)=gcd(x,k),我们发现 g ( x ) g(x) g(x)的取值是非常有限的,只可能是 k k k小于等于 n n n的因子.因此我们可以枚举所有因子的倍数更新 g ( x ) g(x) g(x)的值.

枚举 1 ∼ n 1 \sim n 1n所有倍数的时间复杂度是 O ( n log ⁡ n ) O(n\log{n}) O(nlogn),但因为 k k k的因子在 1 ∼ n 1 \sim n 1n中的分布是非常稀疏的,所以实际复杂度远小于这个上界,时间复杂度不明,但足以通过本题.

代码

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 2e7 + 5, mod = 1e9 + 7;
typedef long long LL;
int primes[maxn], cnt;
int x[maxn], g[maxn];
bool isPrime[maxn];
int qpow(int a, int b, int mod){
    int res = 1;
    while (b){
        if (b & 1) res = 1LL * res * a % mod;
        a = 1LL * a * a % mod;
        b >>= 1;
    }
    return res;
}
int f(int n, LL k){
    int res = 1;
    int up = k % (mod - 1);
    for(int i = 2; i <= n; i++){
        if (!isPrime[i]){
            primes[cnt++] = i;
            x[i] = qpow(i, up, mod);
            g[i] = __gcd(1LL * i, k);
        }
        for(int j = 0; i * primes[j] <= n; j++){
            isPrime[i * primes[j]] = 1;
            x[i * primes[j]] = 1LL * x[i] * x[primes[j]] % mod;
            if (i % primes[j] == 0){
                g[i * primes[j]] = g[i];
                if ((k / g[i]) % primes[j] == 0) g[i * primes[j]] *= primes[j];
                break;
            }
            g[i * primes[j]] = g[i] * g[primes[j]];
        }
        res = (res + 1LL * g[i] * x[i]) % mod;
    }
    return res;
} 
int main(){
    int n; LL k;
    cin >> n >> k;
    cout << f(n, k) << '\n';
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值