第十届“图灵杯”NEUQ-ACM
题目链接
解题报告
D 文稿修订
思路
不断读入字符c,以空格与回车为断点,记录下每一个连续的单词tmp,对于tmp进行相关判断,再加到ans上。
代码
#include <bits/stdc++.h>
using namespace std;
int main(){
char c;
string s;
string tmp;
int cnt = 0;
scanf("%c",&c);
while (1){
if (c == '#') break;
else if (c == ' ' || c == '\n') {
if (tmp == "NEUQ") s += "WOW NEUQ", s += c;
else if (tmp.size() == 4 && (tmp[0]=='N'||tmp[0]=='n') && (tmp[1]=='E'||tmp[1]=='e') && (tmp[2]=='U'||tmp[2]=='u') && (tmp[3]=='Q'||tmp[3]=='q'))
cnt++, s += tmp + c;
else s += tmp + c;
tmp.clear();
}
else {
tmp += c;
}
scanf("%c",&c);
}
cout << cnt << endl << s;
return 0;
}
F 吃包子
思路
利用前缀和求出前i个包子中素包子和肉包子个数,并记录下每个素包子的坐标,之后双指针进行查找。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 5;
int n, m;
int a[maxn], b[maxn];
int c[maxn]; //记录素包子坐标
int cnt;
int main(){
cin >> n >> m;
for (int i = 1; i <= n; i++){
int tmp;
cin >> tmp;
if (tmp == 1){
a[i] = a[i-1] + 1;
b[i] = b[i-1];
}
else {
a[i] = a[i-1];
b[i] = b[i-1] + 1;
c[++cnt] = i;
}
}
if (b[n] <= m) { cout << a[n]; return 0; }
c[++cnt] = n+1;
int ans = 0;
int ll = 0, rr = m + 1;
int l = c[ll]+1, r = c[rr]-1;
while (rr <= cnt){
ans = max(ans, a[r] - a[l-1]);
ll++, rr++;
l = c[ll] + 1, r = c[rr] - 1;
}
cout << ans;
return 0;
}
G 数字鉴定
思路
经典的前缀和问题,可以在每个区间的左端点处+1、区间的右端点处-1,再对序列求一遍前缀和,当某个位置的值大于0时,则其一定被某一个区间所包含。
注意1:右端点是r+1的位置-1
注意2:由于 1e6 是个精确的范围,for循环中不要习惯性地直接1e6 + 5
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5, maxl = 1e6;
int n, q;
int a[maxl + 5];
int asked[maxl + 5];
int main(){
cin >> n >> q;
for (int i = 1; i <= n; i++){
int l, r;
cin >> l >> r;
a[l]++;
a[r + 1]--;
}
for (int i = 1; i <= 1e6; i++){
a[i] += a[i-1];
}
for (int i = 1; i <= q; i++){
int tmp;
cin >> tmp;
if (asked[tmp] == 2) { cout << "YES" << endl; continue;; }
else if (asked[tmp] == 1) { cout << "NO" << endl; continue; }
bool flag = 1;
for (int k = tmp; k <= 1e6; k += tmp){
if (a[k] > 0){
flag = 0;
break;
}
}
if (flag) cout << "YES" << endl, asked[tmp] = 2;
else cout << "NO" << endl, asked[tmp] = 1;
}
return 0;
}
H 线性变换
思路
用vis数组记录当前X是否已经经过,用flag数组记录第一次出现X时是循环节中第几个,用s数组记录循环节中的前缀和。最后ans先加上不在循环节中的数,再加上循环节乘上循环次数。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e6 + 5;
ll n, p, k, b, T;
int a[maxn], flag[maxn];
ll s[maxn];
bool vis[maxn];
int main(){
cin >> n >> p >> k >> b >> T;
for (int i = 0; i < n; i++)
cin >> a[i];
ll sum = 0, cnt = 0;
while (!vis[p]){
cnt++;
vis[p] = 1;
flag[p] = cnt;
s[cnt] = s[cnt - 1] + a[p];
p = (k * p + b) % n;
if (cnt == T) { cout << s[cnt]; return 0; }
}
sum += s[flag[p] - 1];
ll cir = s[cnt] - s[flag[p] - 1];
int cirl = cnt - flag[p] + 1;
T -= flag[p] - 1;
sum += T / cirl * cir + s[flag[p] - 1 + T % cirl] - s[flag[p] - 1];
cout << sum;
return 0;
}
I 试题排版
思路
完全背包的计数变型。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e3 + 5;
typedef long long ll;
ll f[maxn];
int n;
int main(){
cin >> n;
f[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = 0; j <= n; j++)
if (j >= i)
f[j] += f[j - i],
f[j] %= 998244353;
cout << f[n];
return 0;
}
J QQ群
思路
拓扑排序。每次将入度为0的点入队列,结束后没有入队列的点即在环内,并在每次bfs中寻找最长的链长,最后结果即为环内结点数加上最长的链长。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e4 + 5;
int n;
int a[maxn], in[maxn], dis[maxn];
int main(){
cin >> n;
for (int i = 1; i <= n; i++){
cin >> a[i];
in[a[i]]++;
}
queue <int> q;
for (int i = 1; i <= n; i++){
if (!in[i]) q.push(i), dis[i] = 1;
}
int cnt = 0, mx = 0;
while (!q.empty()){
int x = q.front();
q.pop();
mx = max(mx, dis[x]);
cnt++;
dis[a[x]] = max(dis[a[x]], dis[x] + 1);
in[a[x]]--;
if (!in[a[x]] && a[x]) q.push(a[x]);
}
cout << n + 1 - cnt + mx;
return 0;
}
K 跳跃
思路
期望DP,可以通过后缀和进行优化。
代码
#include <bits/stdc++.h>
using namespace std;
#define MOD 998244353
typedef long long ll;
const int maxn = 2e5;
ll n;
ll a[maxn + 5], l[maxn + 5], r[maxn + 5];
ll s[maxn + 5], dp[maxn + 5];
ll qpower(ll a, ll b){
ll ans = 1;
while (b){
if (b & 1) ans *= a;
a *= a;
b >>= 1;
ans %= MOD;
a %= MOD;
}
return ans;
}
int mod_p(int x){
return (x % MOD + MOD ) % MOD;
}
int main(){
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> l[i];
for (int i = 1; i <= n; i++) cin >> r[i];
for (int i = n; i >= 1; i--){
int l1 = min(n + 1, i + l[i]), r1 = min(n + 1, i + r[i]);
ll len = r[i] - l[i] + 1;
ll p = qpower(len, MOD - 2) % MOD;
dp[i] = mod_p(s[l1] - s[r1 + 1]) * p % MOD + a[i];
dp[i] %= MOD;
s[i] = s[i + 1] + dp[i];
s[i] %= MOD;
}
cout << dp[1];
return 0;
}
在模意义下,除法的本质是乘以逆元。具体来说,对于一个正整数 a a a和模数 p p p, a a a在模 p p p意义下的逆元定义为一个正整数 b b b,满足 a b ≡ 1 ( m o d p ) ab\equiv 1\pmod{p} ab≡1(modp)。如果 p p p是质数,那么根据费马小定理, a p − 1 ≡ 1 ( m o d p ) a^{p-1}\equiv 1\pmod{p} ap−1≡1(modp),因此 a a a在模 p p p意义下的逆元为 a p − 2 a^{p-2} ap−2。
根据费马小定理,如果 p p p是一个质数, a a a是一个整数,且 a a a和 p p p互质,那么 a p − 1 ≡ 1 ( m o d p ) a^{p-1} \equiv 1 \pmod p ap−1≡1(modp)。将 p p p替换成 m o d mod mod, a a a替换成 ( r i − l i + 1 ) (r_i-l_i+1) (ri−li+1),得到:
( r i − l i + 1 ) m o d − 1 ≡ 1 ( m o d m o d ) (r_i-l_i+1)^{mod-1} \equiv 1 \pmod {mod} (ri−li+1)mod−1≡1(modmod)
我们可以将这个式子两边同时乘以 ( r i − l i + 1 ) − 1 (r_i-l_i+1)^{-1} (ri−li+1)−1,得到:
( r i − l i + 1 ) − 1 ≡ ( r i − l i + 1 ) m o d − 2 ( m o d m o d ) (r_i-l_i+1)^{-1} \equiv (r_i-l_i+1)^{mod-2} \pmod {mod} (ri−li+1)−1≡(ri−li+1)mod−2(modmod)
因为 ( r i − l i + 1 ) (r_i-l_i+1) (ri−li+1)和 m o d mod mod互质,所以 ( r i − l i + 1 ) (r_i-l_i+1) (ri−li+1)在模 m o d mod mod下一定有逆元,即存在一个整数 b b b,使得 ( r i − l i + 1 ) ⋅ b ≡ 1 ( m o d m o d ) (r_i-l_i+1)\cdot b \equiv 1 \pmod {mod} (ri−li+1)⋅b≡1(modmod)。而 b b b恰好等于 ( r i − l i + 1 ) − 1 (r_i-l_i+1)^{-1} (ri−li+1)−1,所以我们可以将原式中的 ( r i − l i + 1 ) − 1 (r_i-l_i+1)^{-1} (ri−li+1)−1替换成 b b b,得到:
b ≡ ( r i − l i + 1 ) m o d − 2 ( m o d m o d ) b \equiv (r_i-l_i+1)^{mod-2} \pmod {mod} b≡(ri−li+1)mod−2(modmod)
这就是一个关于逆元的常用公式。因此,可以使用这个公式来求解 ( r i − l i + 1 ) − 1 (r_i-l_i+1)^{-1} (ri−li+1)−1。
L 我把你背回来的
思路
按两个不同图跑两次dijkstra,由于dijkstra是求单源最短路径,所以反向建图,这样以N为起点求到其他点的最短路。然后再扫一遍每一个点和它的联通点,如果联通点的最短路径不等于该点最短路径加某张图中两个点之间的路径长度,则磨损值+1,再根据磨损值跑一遍dijkstra,选出磨损值最小的路径。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 3e5 + 5, maxm = 3e5 + 5;
int n, m;
struct node{
int v, d1, d2;
};
vector <node> a[maxm], G[maxm];
bool vis[maxn];
int dis[maxn], dis2[maxn], dis3[maxn];
struct nod{
int pos, dis;
bool operator<(const nod a) const {
return (a.dis < dis);
}
};
priority_queue <nod> q;
int main(){
cin >> n >> m;
for (int i = 0; i < m; i++) {
int u, v, d1, d2;
cin >> u >> v >> d1 >> d2;
a[v].push_back({u, d1, d2});
a[u].push_back({v, d1, d2});
}
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
dis[n] = 0;
q.empty();
q.push({n, 0});
while (!q.empty()) {
int pos = q.top().pos;
q.pop();
if (vis[pos]) continue;
vis[pos] = 1;
for (int i = 0; i < a[pos].size(); i++){
int to = a[pos][i].v;
int dr = a[pos][i].d1;
if (dis[to] > dis[pos] + dr) {
dis[to] = dis[pos] + dr;
q.push({to, dis[to]});
}
}
}
memset(dis2, 0x3f, sizeof(dis2));
memset(vis, 0, sizeof(vis));
dis2[n] = 0;
q.empty();
q.push({n, 0});
while (!q.empty()) {
int pos = q.top().pos;
q.pop();
if (vis[pos]) continue;
vis[pos] = 1;
for (int i = 0; i < a[pos].size(); i++){
int to = a[pos][i].v;
int dr = a[pos][i].d2;
if (dis2[to] > dis2[pos] + dr) {
dis2[to] = dis2[pos] + dr;
q.push({to, dis2[to]});
}
}
}
for (int i = 1; i <= n; i++) {
int len = a[i].size();
for (int j = 0; j < len; j++) {
int to = a[i][j].v;
int cnt = 0;
if (dis[to] != dis[i] + a[i][j].d1) cnt++;
if (dis2[to] != dis2[i] + a[i][j].d2) cnt++;
G[to].push_back({i, cnt});
}
}
memset(dis3, 0x3f, sizeof(dis3));
memset(vis, 0, sizeof(vis));
dis3[1] = 0;
q.empty();
q.push({1, 0});
while (!q.empty()) {
int pos = q.top().pos;
q.pop();
if (vis[pos]) continue;
vis[pos] = 1;
for (int i = 0; i < G[pos].size(); i++){
int to = G[pos][i].v;
int dr = G[pos][i].d1;
if (dis3[to] > dis3[pos] + dr) {
dis3[to] = dis3[pos] + dr;
q.push({to, dis3[to]});
}
}
}
cout << dis3[n];
return 0;
}
M 粉色头发的可爱女孩
思路
状压DP,用一个20位的二进制数来表示每个角色身上的标签,如果第i位上是1那么就表示该角色拥有第i个标签。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1 << 21;
int n, k;
int chr[maxn], who[maxn];
int main(){
cin >> n >> k;
for (int i = 1; i <= n; i++) {
int charm, s, t, x = 0;
cin >> charm >> s;
while (s--) {
cin >> t;
x |= (1 << (t - 1));
}
if (charm > chr[x]) {
chr[x] = charm;
who[x] = i;
}
}
for (int i = (1 << k) - 1; i >= 0; i--){
for (int j = 0; j < k; j++){
if (i >> j & 1) {
int s = i ^ (1 << j);
if (chr[s] < chr[i]){
chr[s] = chr[i];
who[s] = who[i];
}
}
}
}
int q;
cin >> q;
for (int i = 1; i <= q; i++){
int s;
cin >> s;
int x = 0, t;
while (s--) {
cin >> t;
x |= (1 << (t - 1));
}
if (who[x]) cout << who[x] << endl;
else cout << "OMG!" << endl;
}
return 0;
}
实现原理:
枚举所有的子集,然后在子集中寻找一个元素,去掉它,得到另一个子集,然后通过比较两个子集的得分,更新最高分数和对应的玩家编号。
首先,我们从最大的子集开始枚举,即 i = ( 1 < < k ) − 1 i=(1<<k)-1 i=(1<<k)−1,其中 k k k 是题目中给定的集合中元素的数量, < < << << 表示按位左移运算符,相当于将 1 1 1 左移 k k k 位。这个子集包含了所有元素。
然后,我们在这个子集 i i i 中寻找每个元素 j j j,使用右移运算符 > > >> >> 检查元素 j j j 是否在子集 i i i 中。如果 i > > j i>>j i>>j & 1 1 1 为真,说明元素 j j j 在子集 i i i 中。
接下来,我们将元素 j j j 移出子集 i i i,得到一个新的子集 u = i u=i u=i^ ( 1 < < j ) (1<<j) (1<<j),其中 ^ 表示按位异或运算符,相当于将子集 i i i 中的元素 j j j 置为 0 0 0。
然后,我们比较子集 u u u 的得分 f [ u ] f[u] f[u] 和子集 i i i 的得分 f [ i ] f[i] f[i],如果 f [ u ] < f [ i ] f[u]<f[i] f[u]<f[i],说明 i i i 的得分更高,于是我们将 f [ u ] f[u] f[u] 更新为 f [ i ] f[i] f[i],并将 p l a y e r [ u ] player[u] player[u] 更新为 p l a y e r [ i ] player[i] player[i]。
最后,我们可以使用类似的方法处理每个查询:将查询中的元素转换为一个子集,然后查找该子集的最高得分和对应的玩家编号即可。
综上所述,这段代码实现了使用状压DP算法,它能够快速查询给定子集的最高得分和对应的玩家编号。