AT_abc217 复盘
A
非常水
AC code:
#include <bits/stdc++.h>
using namespace std;
string s1, s2;
int main(){
cin >> s1 >> s2;
if (s1 < s2) cout << "Yes";
else cout << "No";
return 0;
}
B
还是非常水,随便拿 map 搞搞
AC code:
#include <bits/stdc++.h>
using namespace std;
string s1, s2, s3;
map<string, int> mp;
int main(){
cin >> s1 >> s2 >> s3;
mp[s1] = 1, mp[s2] = 1, mp[s3] = 1;
string a = "ABC", b = "ARC", c = "AGC", d = "AHC";
if (mp[a] == 0) cout << a;
else if (mp[b] == 0) cout << b;
else if (mp[c] == 0) cout << c;
else if (mp[d] == 0) cout << d;
return 0;
}
C
依旧很水,直接映射一下,甚至不用离散化
AC code:
#include <bits/stdc++.h>
using namespace std;
int n;
int a[200005], b[200005];
int main(){
cin >> n;
for (int i = 1; i <= n; i++){
cin >> a[i];
b[a[i]] = i;
}
for (int i = 1; i <= n; i++) cout << b[i] << " ";
return 0;
}
D
有点难度,但不多。很容易想到二分一下找上界和下界,所以要维护一个数组使它插入的时候自动排好序,用 set
注意:用 set 的时候一定要用自带的二分 st.lower_bound
不然会 TLE
警钟敲烂!!!!
AC code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, q;
set<int> st;
signed main(){
cin >> n >> q;
st.insert(0), st.insert(n);
for (int i = 1, opt, x; i <= q; i++){
cin >> opt >> x;
if (opt == 1) st.insert(x);
else{
auto it = st.lower_bound(x);
cout << *it - *(--it) << endl;
}
}
return 0;
}
E
比 D 水,直接拿一个优先队列和普通队列处理一下就好了
AC code:
#include <bits/stdc++.h>
using namespace std;
int n;
queue<int> q;
priority_queue<int, vector<int>, greater<int>> q1;
int main(){
cin >> n;
for (int i = 1, opt, x; i <= n; i++){
cin >> opt;
if (opt == 1){
cin >> x;
q.push(x);
}
else if (opt == 2){
if (q1.empty()) cout << q.front() << endl, q.pop();
else cout << q1.top() << endl, q1.pop();
}
else{
while (!q.empty()) q1.push(q.front()), q.pop();
}
}
return 0;
}
F
”遇到组合计数的题要么是数学,要么是 dp。“
一开始见到这题感觉是数学,但是这是区间 dp。定义 \(dp_{i,j}\) 表示把 \([i,j]\) 完全消掉的方案数。
不难想到转移,如果 \(l\) 和 \(r\) 是好朋友,那么 \(dp_{l,r}=dp_{l + 1, r - 1}\)。考虑一般情况,枚举断点 \(k\),如果 \(k\) 和 \(r\) 也是朋友关系,就可以做到先把 \([l,k-1]\) 的消掉,再消 \([k + 1,r-1]\) 最后再把 \(k,r\) 消掉,也就是 \(dp_{l,r}=dp_{l, k-1} \times dp_{k + 1,r - 1}\),但是这种没考虑删的顺序,所以还要乘上一个 \(C_{\frac{r - l + 1}{2}}^{\frac{r-k+1}{2}}\)。注意不是阶乘,因为这两个区间内的先后顺序是确定的,只是在两个区间混起来消除的时候考虑这一次消左边还是消右边。
注意在 \(dp\) 初始化时要 \(dp_{u,v}=dp_{v,u}=1\),不然在后面枚举断点的时候可能会出现 \(k=r-1\) 的情况,然后计算的时候就 \(k+1>r-1\) 了,但是还是算方案数的,所以反向也要记录一下。
AC code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 998244353;
int n, m;
int dp[405][405], vis[405][405], c[405][405];
signed main(){
for (int i = 0; i <= 400; i++){
for (int j = 0; j <= i; j++){
if (i == 0) c[i][j] = 1;
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}
}
cin >> n >> m;
for (int i = 1, u, v; i <= m; i++){
cin >> u >> v;
if ((v - u + 1) % 2) continue;
vis[u][v] = 1;
if (u + 1 == v) dp[u][v] = dp[v][u] = 1;
}
for (int len = 2; len <= 2 * n; len += 2){
for (int l = 1; l + len - 1 <= 2 * n; l++){
int r = l + len - 1;
if (vis[l][r]) dp[l][r] = dp[l + 1][r - 1];
for (int k = l + 2; k < r; k += 2){
if (vis[k][r]) dp[l][r] = (dp[l][r] + dp[l][k - 1] * dp[k + 1][r - 1] % mod * c[len / 2][(r - k + 1) / 2] % mod) % mod;
}
}
}
cout << dp[1][2 * n];
return 0;
}
G
比 F 简单一些。前置知识:斯特林数。
先来考虑简单情况,把 \(n\) 个数分成 \(m\) 组有多少种情况?
可以定义 \(dp_{i,j}\) 表示第 \(i\) 个数之前分了 \(j\) 组有多少种情况。
转移分两种讨论:
-
\(i\) 单独开一组新的:\(dp_{i,j}=dp_{i-1,j-1}\)。
-
把 \(i\) 放到前面的几组中:\(dp_{i,j}=dp_{i-1,j} \times j\)。乘上 \(j\) 表示可以放到前 \(j\) 组,每组 \(dp_{i-1,j}\) 种情况。
回到原题,因为有了模数的限制,问题肯定出在放到前 \(j\) 组的地方,那么要乘上多少呢?就是除了放了模数相同的数的组里。而我们已经知道前 \(i-1\) 个数中与 \(i\) 同余的数肯定不在一个格子内,而有 \(\lfloor \frac{i-1}{m}\rfloor\) 个与 \(i\) 同余的数,所以有 \(j-\lfloor \frac{i-1}{m}\rfloor\) 组是合法的,可得最后式子为:
\[dp_{i,j}=dp_{i-1,j-1}+dp_{i-1,j} \times (j-\lfloor \frac{i-1}{m}\rfloor) \]还有一点,在转移的时候记得将合法组数和 \(0\) 取一个 \(\max\),肯定不能取负的组数嘛。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 998244353;
int n, m;
int dp[5005][5005];
signed main(){
cin >> n >> m;
dp[0][0] = 1;
for (int i = 1; i <= n; i++){
for (int j = 1; j <= i; j++){
dp[i][j] = (dp[i][j] + dp[i - 1][j - 1]) % mod;
dp[i][j] = (dp[i][j] + dp[i - 1][j] * max((j - (i - 1) / m), 0ll)) % mod;
}
}
for (int i = 1; i <= n; i++) cout << dp[n][i] << endl;
return 0;
}
AT_abc219 复盘
A
直接上 if 大法
AC code:
#include <bits/stdc++.h>
using namespace std;
int x;
int main(){
cin >> x;
if (x < 40) cout << 40 - x;
else if (x >= 40 && x < 70) cout << 70 - x;
else if (x >= 70 && x < 90) cout << 90 - x;
else cout << "expert";
return 0;
}
B
暴力字符串拼接
AC code:
#include <bits/stdc++.h>
using namespace std;
string s[5], t, ans;
int main(){
cin >> s[1] >> s[2] >> s[3] >> t;
for (int i = 0; i < t.size(); i++) ans += s[t[i] - '0'];
cout << ans;
return 0;
}
C
好像直接写一个 \(cmp\) 也可以
拿一个 map 映射新字符对应着哪一个旧字符。询问的时候转换一下新旧字符,再拿个 map 映射一下,最后输出
AC code:
#include <bits/stdc++.h>
using namespace std;
int n;
string s, t;
map<char, char> mp;
map<string, string> pre;
vector<string> v;
int main(){
cin >> s;
for (int i = 0; i < s.size(); i++) mp[s[i]] = i + 'a';
cin >> n;
for (int i = 1; i <= n; i++){
cin >> t;
string tmp = t;
for (int i = 0; i < t.size(); i++) tmp[i] = mp[t[i]];
v.push_back(tmp);
pre[tmp] = t;
}
sort(v.begin(), v.end());
for (int i = 0; i < n; i++) cout << pre[v[i]] << endl;
return 0;
}
D
很明显是一个 dp 题。
按照朴素的想法,\(dp_{i,j,k}\) 表示前 \(i\) 个盒饭 \(j\) 个 a 种, \(k\) 个 b 种,很明显这样会 RE。
转换一下 \(dp_{i,j,k}\) 表示前 \(i\) 个盒饭大于等于 \(j\) 个 a 种,大于等于 \(k\) 个 b 种,这样子只用开到 \(300 \times 300 \times 300\) 就够了。
转移分两种:
- 取 \(i - 1\) 个盒饭就已经满足要求,\(dp_{i,j,k}=dp_{i-1,j,k}\)
- 取第 \(i\) 个盒饭,\(dp_{i,j,k}=\min(dp_{i,j,k}, dp_{i-1,j-a[i],k-b[i]}+1)\)
basecase 很好想,肯定就是 \(dp_{0,0,0}=0\)。ans 就是 \(dp_{n,x,y}\)
AC code:
#include <bits/stdc++.h>
using namespace std;
int n, x, y;
int a[305], b[305], dp[305][305][305];
int main(){
cin >> n >> x >> y;
for (int i = 1; i <= n; i++) cin >> a[i] >> b[i];
memset(dp, 0x3f, sizeof(dp));
dp[0][0][0] = 0;//basecase
for (int i = 1; i <= n; i++){
for (int j = 0; j <= x; j++){
for (int k = 0; k <= y; k++){
if (dp[i - 1][j][k] != 0x3f3f3f3f) dp[i][j][k] = dp[i - 1][j][k];//直接满足条件
dp[i][j][k] = min(dp[i][j][k], dp[i - 1][max(j - a[i], 0)][max(k - b[i], 0)] + 1);//转移,记得特判一下边界
}
}
}
cout << (dp[n][x][y] == 0x3f3f3f3f ? -1 : dp[n][x][y]);
return 0;
}
E
数据范围很容易想到状压,但又很容易误导成状压 dp。
只需要枚举那个点在护城河内,在去判断不合法的情况。第 4、5 种情况已经被排除了,只需要判断前三种情况。
第一种很好判断,第二种也只需要去判一下连通块是否只有一个,而第三种就有点难度了。我们发现,如果状压出来的护城河由两条的话肯定是围成了一个类似于大圆套小圆的结构,护城河包裹的是圆环,而中间那部分在状压中是没有被包裹的,这就只需要判断是否有一块没有选中的点是走不到边缘的,如果有,那一定是被圆环包裹住了,也就是不合法的情况。
最后统计答案。
第二种:
第三种:
AC code:
#include <bits/stdc++.h>
using namespace std;
const int dx[4] = {0, 1, 0, -1};
const int dy[4] = {1, 0, -1, 0};
int n = 4, ans;
int a[5][5], b[5][5], vis[5][5];
int dfs1(int x, int y){//找被护城河围起来了但不是城内的区域
if (x <= 0 || x > n || y <= 0 || y > n) return 1;
if (b[x][y]) return 0;
vis[x][y] = 1;
int flag = 0;
for (int i = 0; i < 4; i++){
int nx = x + dx[i], ny = y + dy[i];
if (vis[nx][ny]) continue;
flag |= dfs1(nx, ny);
}
return flag;
}
void dfs(int x, int y){//找连通块
vis[x][y] = 1;
for (int i = 0; i < 4; i++){
int nx = x + dx[i], ny = y + dy[i];
if (nx > 0 && nx <= n && ny > 0 && ny <= n && vis[nx][ny] == 0 && b[nx][ny] == 1) dfs(nx, ny);
}
}
int main(){
for (int i = 1; i <= n; i++){
for (int j = 1; j <= n; j++) cin >> a[i][j];
}
for (int k = 1; k < (1 << (n * n)); k++){
for (int i = 0; i < n * n; i++) b[i / n + 1][i % n + 1] = 1 & (k >> i);
memset(vis, 0, sizeof(vis));
int flag = 1, cnt = 0;
for (int i = 1; i <= n && flag; i++){
for (int j = 1; j <= n && flag; j++){
if (a[i][j] == 1 && b[i][j] == 0) flag = 0;
else if (b[i][j] == 0 && !vis[i][j]) flag = dfs1(i, j);
else if (b[i][j] == 1){
if (cnt && !vis[i][j]) flag = 0;
else dfs(i, j), cnt++;
}
}
}
if (flag) ans++;
}
cout << ans;
return 0;
}
F
updating...
AC code:
G
根号分治,简单来说就是小于某一个值和大于某个值分开来做。
这题如果用暴力就是 \(n^2\) 级别的,会炸。在想优化的过程中,我们发现如果把所有要修改的点都打上标记的话,还是在 \(n^2\) 级别的,但是变成了修改 \(O(1)\),查询 \(O(n)\) 的东西,这种跟分块有点类似——就是根号分治。我们的目的就是平摊复杂度,修改 \(\sqrt n\),查询 \(\sqrt{n}\)。
如何分块?只要把点分成两类,重点和轻点——重点代表所连边数大于 \(\sqrt{m}\),轻点则相反。
轻点就可以直接在操作的时候暴力修改,而重点就可以打一个类似于线段树的 lazytag,包含一个当前颜色 color 和修改时间 time。注意,这里的 tag 是指这个重点放出的颜色的 color 和 time,而不是这个点最终颜色。
还需要预处理一下,再建一个图,只由轻点和所连接的重点构成。
具体操作时,先把要修改的那个点还原出来,也就是把与它邻接的重点的 tag 扒出来,找到修改时间最晚并且晚于当前节点修改时间的重点,覆盖掉当前节点。
并更新当前节点覆盖别的节点的 time。然后分治重点和轻点,轻点直接更新,重点打上一个 tag,等到下次要用到这个重点的时候再按上一个步骤中的操作还原到需要的节点上。
对于复杂度的证明,重点不超过 \(\sqrt{m}\) 个,于是还原节点最多 \(O(\sqrt{m})\);打上 tag 的复杂度为 \(O(1)\);轻点所连的边不超过 \(\sqrt{m}\),更新一次复杂度也不超过 \(\sqrt{m}\)。
预处理阶段,只连接轻点的重点,不超过 \(n \sqrt{n}\);最后输出操作也不超过 \(n \sqrt{m}\)。总复杂度在 \(n\sqrt{n}\) 级别,可以过。
AC code:
#include <bits/stdc++.h>
using namespace std;
int n, m, q, len;
int col[200005], tim[200005];//最终答案和标记时间
int col1[200005], tim1[200005];//重点放出的color和放出时间
vector<int> g[200005], e[200005];//完整的图和每一个轻点连向重点的图
int find(int x){//找到更新时间最晚的重点
int mxt = tim[x], tmp = col[x];//先记录当前节点更新时间,便于后面判断是否晚于当前节点
for (int i = 0; i < e[x].size(); i++){
int v = e[x][i];
if (mxt < tim1[v]) mxt = tim1[v], tmp = col1[v];
}
return tmp;
}
int main(){
cin >> n >> m >> q;
len = sqrt(m);
for (int i = 1; i <= n; i++) col[i] = i;
for (int i = 1, u, v; i <= m; i++){
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
for (int i = 1; i <= n; i++){
if (g[i].size() < len) continue;
for (int j = 0; j < g[i].size(); j++) e[g[i][j]].push_back(i);//只记录轻点连向重点的边,达到 根号 m 的复杂度
}
for (int _ = 1, x; _ <= q; _++){
cin >> x;
col[x] = find(x), tim[x] = _;
if (g[x].size() >= len) col1[x] = col[x], tim1[x] = _;//重点打上lazytag
else{
for (int i = 0; i < g[x].size(); i++) col[g[x][i]] = col[x], tim[g[x][i]] = _;//轻点直接更新
}
}
for (int i = 1; i <= n; i++) cout << find(i) << " ";//跟上面还原 col[x] 一个道理
return 0;
}
AT_abc238 复盘
A
挺简单的,简单画个图或者枚举一下
AC code:
#include <bits/stdc++.h>
using namespace std;
int n;
int main(){
cin >> n;
if (n >= 2 && n <= 4) cout << "No";
else cout << "Yes";
return 0;
}
B
把每个切点丢到数组里或优先队列,然后一个一个出队看最大的块
注意要把 0 和 360 也放进去
AC code:
#include <bits/stdc++.h>
using namespace std;
int n, a, lst, ans;
priority_queue<int> q;
int main(){
cin >> n;
q.push(0);
for (int i = 1; i <= n; i++){
cin >> a;
lst = (lst + a) % 360;
q.push(lst);
}
q.push(360);
while (q.size() > 1){
int x = q.top(); q.pop();
int y = q.top();
ans = max(ans, x - y);
}
cout << ans;
return 0;
}
C
简简单单一个公式,稍微在草稿纸上推一下每一位的公式
but 在写完之后公式错了。。。加上有一个地方没有模调了 1h。。。
AC code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 998244353;
int n, ans;
signed main(){
cin >> n;
int lst = 0;
for (; pow(10, lst + 1) <= n; lst++){
ans = (ans + (1 + (int)pow(10, lst + 1) - (int)pow(10, lst)) % mod * (((int)pow(10, lst + 1) - (int)pow(10, lst)) % mod) / 2) % mod;
}
ans = (ans + (1 + n - (int)pow(10, lst) + 1) % mod * ((n - (int)pow(10, lst) + 1) % mod) / 2) % mod;
cout << ans;
return 0;
}
D
D 有点抽象,但不多
首先考虑结论,容易得到如果 \(a \ge s\) 时肯定不行
之后举几个例子发现 \(s\) 减去了 \(x,y\) 中的两个 \(a\) 之后剩下的数合法情况与 \(a\) 是为 \(0\) 的,而且这个剩下的数大于 \(0\)。
很简单,如果剩下的数那几位还是 \(1\),就说明 \(x\) 和 \(y\) 中有一位是 \(1\) (不可能是进位上来的,前面都是最多一位为 \(1\)),那么加上 \(a\) 再与会发现这一位与出来是 \(0\)。
AC code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
int t, a, b;
signed main(){
cin >> t;
while (t--){
cin >> a >> b;
int s = b - 2 * a;
if (s < 0 || (s & a) != 0) cout << "No\n";
else cout << "Yes\n";
}
return 0;
}
E
很难想到可以用并查集。
在输入每一个数据的时候建一条 \((u-1,v)\) 的边,并放入并查集。最后判断 \(0\) 和 \(n\) 是否联通。
非常神奇,模拟一下发现这就等同于要求有好几个区间连续拼成一整个区间。
AC code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, q;
int fa[200005];
int find(int x){
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y){
fa[find(y)] = find(x);
}
signed main(){
cin >> n >> q;
for (int i = 1; i <= n; i++) fa[i] = i;
for (int i = 1, u, v; i <= q; i++){
cin >> u >> v;
merge(u - 1, v);
}
cout << ((find(0) == find(n)) ? "Yes" : "No");
return 0;
}
F
贪心+dp
先按 \(a\) 排一边序,定义 \(dp_{i,j,k}\) 表示前 \(i\) 位取了 \(j\) 位未选最小值为 \(k\)。
考虑转移,我们发现枚举到 \(i\) 时通过 \(i-1\) 转移不好转,考虑从 \(i\) 转到 \(i + 1\)。
分两种情况讨论:
- 取这个数,因为 \(a_i\) 肯定大于前面的,只能是 \(b_i < k\) 才能转,\(dp_{i + 1,j + 1,k}+=dp_{i,j,k}\)。
- 不取这个数,那么就要转移到 \(min(k,b_i)\) 上,\(dp_{i+1,j,\min{b_i},k}+=dp_{i,j,k}\)。
最后答案就是 \(\sum_{i=1}^{n+1}{dp_{n,m,i}}\),这里 \(n\) 可能等于 \(k\),所以要枚举到 \(n+1\)。
AC code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 998244353;
int n, m, ans;
int dp[305][305][305];
struct node{
int p, q;
}a[305];
bool cmp(node x, node y){
return x.p < y.p;
}
signed main(){
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i].p;
for (int i = 1; i <= n; i++) cin >> a[i].q;
sort(a + 1, a + n + 1, cmp);
dp[0][0][n + 1] = 1;
for (int i = 0; i < n; i++){
for (int j = 0; j <= m; j++){
for (int k = 1; k <= n + 1; k++){
if (a[i + 1].q < k && j != m) dp[i + 1][j + 1][k] = (dp[i + 1][j + 1][k] + dp[i][j][k]) % mod;
dp[i + 1][j][min(k, a[i + 1].q)] = (dp[i + 1][j][min(k, a[i + 1].q)] + dp[i][j][k]) % mod;
}
}
}
for (int i = 1; i <= n + 1; i++) ans = (ans + dp[n][m][i]) % mod;
cout << ans;
return 0;
}
标签:abc,int,namespace,cin,2024,code,include,复盘,dp
From: https://www.cnblogs.com/ccf-ioi/p/18062554