2023信友队提高组复赛冲刺班 10.1赛后总结
T1:鸭运会
这是一道贡献问题 。由题目可以知道,t总=t走+t等 ,因此可以分别计算t走和t等对t总的贡献。
对于前者,由于是所有子区间的和,且走过一个红绿灯的时间均为1个单位,所以,t走 =½n(1+n)
对于后者,要判断在什么时候需要等待,这稍微会复杂一些,但通过多次的模拟,我们可以发现如下性质和规律:
1.所有红绿灯的状态只有两种
例如,对于样例中的11010,它只有两种状态:11010和00101
2.每一对红绿灯之间的颜色关系(同色或不同色)是固定的(由第1点可以推出)
还是使用1中的样例11010,例如,对于位置1和位置3的红绿灯,不论怎么变化,它们永远都是不同色的;对于位置2和位置4的红绿灯,不论怎么变化,它们永远都是同色的
3.如果走过了当前的红绿灯,那么上一个红绿灯一定是红灯;反之,若在当前红绿灯等待,那么上一个红绿灯一定是绿灯(由第1点可以推出)
这很明显,因为每走一次,所有红绿灯都是要变化的
受到如上性质和规律的启发,我们可以由一个红绿灯当前的状态推出其他红绿灯的状态。但是要注意特殊情况,就是如果当前红绿灯没有上一个红绿灯,那么可以由其初始状态推出其当前状态。
综上所述,当且仅当某红绿灯是从p向右走遇到的第一个红绿灯且其种类为0,或某红绿灯不是从p向右走遇到的第一个红绿灯且与前一个红绿灯种类相同时,需要等待。
AC CODE
1 #include<bits/stdc++.h> 2 using namespace std; 3 4 void solve(){ 5 int n;string s; 6 long long res=0,sum=0; 7 cin>>n>>s; 8 for(int i=0;i<n;i++){ 9 sum+=i+1; 10 res+=sum; 11 if(~s[i]&1)res+=n-i; 12 if(i&&s[i]==s[i-1])res+=1LL*i*(n-i); //比赛的时候没写1LL,见祖宗去了,才40pts 13 } 14 cout<<res<<"\n"; 15 } 16 17 int main(){ 18 freopen("A.in","r",stdin); 19 freopen("A.out","w",stdout); 20 int T; 21 cin>>T; 22 while(T--)solve(); 23 return 0; 24 }
复杂度:O(n)
T2:正确的
这一道题总算是AC了,上一道题真是让我蓝瘦香菇啊( ̄▽ ̄)"!
这是一道构造题,可以采用一种贪心的策略:尽可能把同种字典序小的字母往前排,最多排k-1个,等到排满的时候,再放一个字典序大于且仅大于这种字母的字母来隔一下避免子串长度超出即可。
所以,只需要写一个check函数来判断一下是否有足够多的其他字母来填充即可,复杂度O(26*26*n)
也许上面的描述太过抽象,那么我们那一个样例来模拟一下:
s='"aabbccdd",k=2
先统计一下各种字母的个数:
a:2个
b:2个
c:2个
d:2个
然后开始构造:
(篇幅有限,此部分省略)
AC CODE
1 #include <bits/stdc++.h> 2 #define N (int)1e5+10 3 using namespace std; 4 char s[N]; 5 int n, k, cnt[26],num_c, last_c, sum,old_num_c, old_last_c; 6 bool check() { 7 bool flag = true; 8 for (int i = 0; i < 26; i++) if (cnt[i]) { 9 if (last_c == i) flag &= cnt[i] <= 1LL * (sum - cnt[i]) * (k - 1) + (k - 1 - num_c); 10 else flag &= cnt[i] <= 1LL * (sum - cnt[i] + 1) * (k - 1); 11 } 12 return flag; 13 } 14 void solve() { 15 cin>>s>>k; 16 n = strlen(s); 17 memset(cnt, 0, sizeof(cnt)); 18 for (int i = 0; i < n; i++) cnt[s[i] - 'a'] += 1; 19 last_c = -1, sum = n; 20 if (k == 1 || !check()) {puts("OH NO!");return;} 21 for (int i = 0; i < n; i++) { 22 sum -= 1; 23 for (int j = 0; j < 26; j++) if (cnt[j]) { 24 if (last_c == j && num_c + 1 == k) continue; 25 cnt[j] -= 1; 26 old_num_c = num_c,old_last_c = last_c; 27 if (last_c == j) num_c += 1; 28 else {last_c = j;num_c = 1;} 29 if (check()) {printf("%c", j + 'a');break;} 30 cnt[j] += 1; 31 num_c = old_num_c,last_c = old_last_c; 32 } 33 } 34 puts(""); 35 } 36 37 int main() { 38 freopen("B.in", "r", stdin); 39 freopen("B.out", "w", stdout); 40 int T; 41 cin>>T; 42 while (T--) solve(); 43 return 0; 44 }
码风丑陋,敬请谅解ε=ε=ε=(~ ̄▽ ̄)~
T3:洞穴蛆虫
题意太过抽象,赛场上直接放弃,去做T4了。出题人讲人话行吗?orz
其实,现在回头看一下,这道题其实就是一道很直接的状压DP,考虑预处理每个连通块在每个深度上的位置数量,从下向上d,设f(i,S)表示在深度i,集合S中的连通块在该深度有蛆虫。转移的时候S可以向其一个子集转移。注意到每个连通块根据其最浅深度与当前是否有蛆虫两个信息,可判断在深度i-1要么必须没蛆虫,要么可有蛆虫可无蛆虫。转移之后,根据xi的限制将不合法的状态清空。使用高维前缀和的方法转移,复杂度O(r·k·2k),其中k是连通块个数。
等一下,这个高维前缀和是个什么东西?
别太惊奇,其实说来也简单,三行代码而已
1 for(int j = 0; j < n; j++) 2 for(int i = 0; i < 1 << n; i++) 3 if(i >> j & 1) f[i] += f[i ^ (1 << j)];
怎么样?惊不惊喜?意不意外?
求解高维前缀和的核心思想也就是一维一维来处理,可以类比二维前缀和的求法稍微模拟一下。
具体来说代码中的f[i] += f[i ^ (1 << j)]因为我们是正序枚举,所以i ^ (1 << j)在当前层,而i还在上层,所以我们将两个合并一下就能求出当前层的前缀和了QAQ。
然后...就完了,好像没什么好说的。
上大家喜闻乐见的代码吧
AC CODE
1 #include <bits/stdc++.h> 2 using namespace std; 3 const int maxn = 25,maxm = 405,INF = 1e9 + 7; 4 int vis[maxn][maxn], cdep[maxn][maxn], ok[maxn][maxn], mn[maxn], mx[maxn], lim[maxn],r, c, tot,num[1 << 25], f[1 << 25]; 5 vector<pair<int, int> > g[maxn][maxn]; 6 vector<pair<int, int> > vec[maxn]; 7 string buf; 8 inline void dfs(int x, int y, int id){ 9 vis[x][y] = 1; 10 vec[id].push_back(make_pair(x, y)); 11 for(auto p: g[x][y]) if(!vis[p.first][p.second]) dfs(p.first, p.second, id); 12 } 13 inline void dfs2(int x, int y, int dep){ 14 vis[x][y] = 1; 15 for(auto p: g[x][y]) if(p.second >= dep && !vis[p.first][p.second]) dfs2(p.first, p.second, dep); 16 } 17 inline int &add(int &x, const int &y){ return (x += y) -= (x >= INF ? INF : 0); } 18 int main(){ 19 freopen("C.in", "r", stdin); 20 freopen("C.out", "w", stdout); 21 cin >> r >> c; 22 getline(cin, buf); 23 for(int i = 0; i < r + 1; ++i){ 24 getline(cin, buf); 25 if(!i) continue; 26 for(int j = 0; j < c * 2 + 1; ++j){ 27 int x = i - 1, y = j / 2; 28 if(j % 2 == 0 && buf[j] != '|'){ 29 g[x][y].push_back(make_pair(x, y - 1)); 30 g[x][y - 1].push_back(make_pair(x, y)); 31 } 32 if(j % 2 == 1 && buf[j] != '_'){ 33 g[x][y].push_back(make_pair(x + 1, y)); 34 g[x + 1][y].push_back(make_pair(x, y)); 35 } 36 } 37 } 38 for(int i = 0; i < r; ++i) for(int j = 0; j < c; ++j) if(!vis[i][j]){dfs(i, j, tot);++tot;} 39 memset(mn, 0x3f, sizeof(mn)); 40 for(int i = 0; i < tot; ++i) 41 for(int j = 0; j < r; ++j) 42 for(int k = 0; k < vec[i].size(); ++k) if(vec[i][k].first == j) ++cdep[i][j], mx[i] = max(mx[i], j), mn[i] = min(mn[i], vec[i][k].first); 43 for(int i = 0; i < r; ++i) scanf("%d", lim + i); 44 lim[r] = -1; 45 for(int S = 0; S < (1 << tot); ++S) f[S] = 1; 46 for(int i = r; i >= 0; --i){ 47 if(~lim[i]){ 48 num[0] = 0; 49 if(num[0] != lim[i]) f[0] = 0; 50 for(int S = 1; S < (1 << tot); ++S){ 51 int b = __builtin_ctz(S); 52 num[S] = num[S ^ (1 << b)] + cdep[b][i]; 53 if(num[S] != lim[i]) f[S] = 0; 54 } 55 } 56 if(i == 0) break; 57 for(int j = 0; j < tot; ++j) 58 for(int S = 0; S < (1 << tot); ++S) 59 if(S >> j & 1){ // dep >= i filled 60 if(mx[j] >= i) add(f[S ^ (1 << j)], f[S]); 61 if(i - 1 < mn[j]) f[S] = 0; 62 } 63 } 64 int ans = 0; 65 for(int S = 0; S < (1 << tot); ++S) add(ans, f[S]); 66 cerr << ans << endl; 67 printf("%d\n", ans); 68 return 0; 69 }
码风愈发狰狞了,多多谅解啊QWQ
T4:鸭鸭魔法阵
考虑使用查分约束系统解决该题。新建一个0号点,向号点连一条长度为ri的单向边,号点向它连一条长度为-li的单向边。那么有解等价于图中不存在负环。
由于所有负边都一定连向0号点,图中存在负环当且仅当图中存在一个经过0号点的简单负环。
到这一步就可以开始二分答案,然后判断更新后的图中是否存在负环。
但是可以发现二分是没有必要的,因为所有经过0的简单环增加的长度都是2×mid,
所以可以直接找出长度最小的经过0号点的简单环,假设这个环长度为l,则答案为max(0,「l/2⌉).
找出长度最小的简单环可以通过dijkstrai或树形DP解决,复杂度O(Tnlogn)或O(Tn)。
后来查了一下,这似乎是一道模板题?
AC CODE
1 #include <bits/stdc++.h> 2 #define maxn 1000010 3 #define ll long long 4 #define fi first 5 #define se second 6 #define pb push_back 7 #define eb emplace_back 8 #define pii pair<int, int> 9 using namespace std; 10 11 int a[maxn], b[maxn], n, type; 12 ll f[maxn], g[maxn], ans; 13 const ll INF = 1LL << 60; 14 vector<pii> nxt[maxn]; 15 template <class T> void read(T &x) { 16 char ch = x = 0; 17 bool fl = false; 18 while (!isdigit(ch)) 19 fl |= ch == '-', ch = getchar(); 20 while (isdigit(ch)) 21 x = x * 10 + ch - '0', ch = getchar(); 22 x = fl ? -x : x; 23 } 24 void clear() { 25 for (int i = 1; i <= n; i++) { 26 nxt[i].clear(); 27 } 28 ans = 0; 29 } 30 void dfs(int x, int fa, ll dis) { 31 f[x] = -dis - b[x], g[x] = -dis + a[x]; 32 for (auto &y : nxt[x]) { 33 if (y.fi != fa) { 34 dfs(y.fi, x, dis + y.se); 35 ans = max(ans, max(f[x] + g[y.fi], g[x] + f[y.fi]) + 2LL * dis); 36 f[x] = max(f[x], f[y.fi]), g[x] = max(g[x], g[y.fi]); 37 } 38 } 39 } 40 void solve() { 41 read(n), clear(); 42 for (int i = 1; i <= n; i++) { 43 read(a[i]); 44 assert(-1e9 <= a[i] && a[i] <= 1e9); 45 } 46 for (int i = 1; i <= n; i++) { 47 read(b[i]); 48 assert(-1e9 <= b[i] && b[i] <= 1e9); 49 } 50 for (int i = 1, u, v, w; i < n; i++) { 51 read(u), read(v), read(w), nxt[u].eb(v, w), nxt[v].eb(u, w); 52 } 53 dfs(1, 0, 0); 54 printf("%lld\n", !type ? !!ans : ans + 1 >> 1); 55 } 56 int main() { 57 freopen("D.in", "r", stdin); 58 freopen("D.out", "w", stdout); 59 int T; 60 for (read(T), read(type); T--; solve()); 61 return 0; 62 }
现在码风好看一些了吧?(。^▽^)
总结与归纳
本次模拟赛,3.5h,总分400pts,实际得分240pts
难度:T2<T1<T4<T3
T1主要考察选手敏锐的观察能力和思维推理能力
T2主要考察选手对贪心算法的应用能力
T3主要考察选手对状压DP和前缀和的综合应用能力
T4主要是为了普及查分约束系统和Dijkstra算法,是一道模板题
完结,撒花*★,°*:.☆( ̄▽ ̄)/$:*.°★* 。
~THE END~