训练!!!!
周一
hdu 5236(概率dp)
首先要猜测一下策略,策略是将n个字符均分,每敲固定的字符就保存一下。
比如将n分成i段,那么每一段的长度就是k或k+1,余数部分就分配到每个k,变成k+1
那么这样就要处理没有保存的情况下,打k个字符的期望
用dp[k]表示打k个字符的期望,注意这时不是逆推,因为需要的就是正推的。
那么有dp[i] = dp[i - 1] + p(1 + dp[i]) + (1 - p) 可化为dp[i] = (dp[i - 1] + 1) / (1 - p)
输出时6位小数即可,我输出了10位反而wa了
#include<bits/stdc++.h>
#define l(k) (k << 1)
#define r(k) (k << 1 | 1)
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
double dp[N], p;
int n, x;
int main()
{
int T, kase = 0;
scanf("%d", &T);
while(T--)
{
scanf("%d%lf%d", &n, &p, &x);
_for(i, 1, n) dp[i] = (dp[i - 1] + 1) / (1 - p);
double ans = 1e18;
_for(i, 1, n)
{
int k = n / i, r = n % i;
ans = min(ans, r * dp[k + 1] + (i - r) * dp[k] + i * x);
}
printf("Case #%d: %.6f\n", ++kase, ans);
}
return 0;
}
迷宫游戏(期望dp+处理方程)
首先要写出方程,这个是简单的。注意这道题要求的是走过的边数的期望,而实际上三种情况只有一种情况会增加一条边,所以写方程时不要直接写1
写出方程后发现有dp[1]和dp[fa[i]]两个未知量,那么就设dp[i] = aidp[1]+bidp[fa[i]],然后代入dp方程中,化成dp[1]和dp[fa[i]]的形式,得到递推式。然后求一下叶子节点的式子即可。
无解的话就是除0的情况。因为一般情况下是可以算出来的,只有分母为0是不能的。
#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e4 + 10;
double k[N], e[N], a[N], b[N], c[N], t[N];
vector<int> g[N];
int n;
bool dfs(int u, int fa)
{
if(g[u].size() == 1 && g[u][0] == fa)
{
a[u] = k[u];
b[u] = 1 - e[u] - k[u];
c[u] = 1 - e[u] - k[u];
return true;
}
double suma = 0, sumb = 0, sumc = 0;
t[u] = (1 - e[u] - k[u]) / g[u].size();
for(int v: g[u])
{
if(v == fa) continue;
if(!dfs(v, u)) return false;
suma += a[v];
sumb += b[v];
sumc += c[v];
}
if(fabs(1 - t[u] * sumb) < 1e-8) return false;
a[u] = (k[u] + t[u] * suma) / (1 - t[u] * sumb);
b[u] = t[u] / (1 - t[u] * sumb);
c[u] = (t[u] * sumc + 1 - e[u] - k[u]) / (1 - t[u] * sumb);
return true;
}
int main()
{
scanf("%d", &n);
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
_for(i, 1, n)
{
int x, y;
scanf("%d%d", &x, &y);
k[i] = 1.0 * x / 100;
e[i] = 1.0 * y / 100;
}
if(!dfs(1, 0) || fabs(1 - a[1]) < 1e-8) puts("impossible");
else printf("%.10f\n", c[1] / (1 - a[1]));
return 0;
}
hdu 3535(混合背包)
这题真的把各种背包结合起来
强制选一个的操作很秀
#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 100 + 10;
int dp[N][N], w[N], v[N], n, T, m, s;
int main()
{
while(~scanf("%d%d", &n, &T))
{
memset(dp, 0, sizeof dp);
_for(i, 1, n)
{
scanf("%d%d", &m, &s);
_for(j, 1, m) scanf("%d%d", &w[j], &v[j]);
if(s == 0)
{
_for(j, 0, T) dp[i][j] = -1e9; //因为必须取一个有不合法的状态,所以初始化为负无穷
_for(k, 1, m)
for(int j = T; j >= w[k]; j--)
dp[i][j] = max(dp[i][j], max(dp[i][j - w[k]] + v[k], dp[i - 1][j - w[k]] + v[k])); //强制选一个,以及和前面的状态做背包
}
else if(s == 1)
{
_for(j, 0, T) dp[i][j] = dp[i - 1][j];
_for(k, 1, m)
for(int j = T; j >= w[k]; j--)
dp[i][j] = max(dp[i][j], dp[i - 1][j - w[k]] + v[k]);
}
else
{
_for(j, 0, T) dp[i][j] = dp[i - 1][j];
_for(k, 1, m)
for(int j = T; j >= w[k]; j--)
dp[i][j] = max(dp[i][j], dp[i][j - w[k]] + v[k]);
}
}
printf("%d\n", (dp[n][T] < 0) ? -1: dp[n][T]);
}
return 0;
}
周二
D. 2+ doors(位运算+贪心)
现在多刷刷cf的题,关键是学解题技巧。
首先是位运算的题很常见的思路就是每一位独立出来考虑,这道题也是一样
首先若边为0,那么肯定端点都是0,这是可以确定的,赋值。
然后考虑什么时候字典序最小,那么就从前往后,每一位看能不能为0
必须为1只有一种情况,就是边为1,而另一个端点为0。对于当前点,前面的点在已经确定了,为了尽量使得当前可以为1,我们可以把后面的位先全部设为1(除了零边赋值为0)
那么就遍历当前点所有的边,看是否有不得不为1的情况,即一边且另一个端点是0。这样一位一位即可。有一个细节就是可能自己向自己连边,这个时候是一边的话那这一位就肯定为1了。
#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 2e5 + 10;
int ans[N], t[N], n, m;
vector<pair<int, int>> g[N];
int main()
{
scanf("%d%d", &n, &m);
while(m--)
{
int i, j, x;
scanf("%d%d%d", &i, &j, &x);
if(i > j) swap(i, j);
g[i].push_back({j, x});
g[j].push_back({i, x});
}
_for(j, 0, 30)
{
_for(i, 1, n) t[i] = 1;
_for(i, 1, n)
for(auto x: g[i])
if(!((x.second >> j) & 1))
t[i] = t[x.first] = 0;
_for(i, 1, n)
{
if(!t[i]) continue;
int flag = 1;
for(auto x: g[i])
if(!t[x.first] || i == x.first)
{
flag = 0;
break;
}
if(flag) t[i] = 0;
}
_for(i, 1, n)
if(t[i])
ans[i] |= 1 << j;
}
_for(i, 1, n) printf("%d ", ans[i]);
return 0;
}
D. Difference Array(暴力+优化)
这是一类题,暴力看似不可行,实际上是可行的,加一些小优化,或者是复杂度计算。
这题的关键就是发现计算的过程中会出现很多0,把0单独考虑可以大大加快速度,这样暴力也能过。
考虑0的话,维护最后的0的位置,每次计算和排序都考虑后面的数,然后继续维护0的位置。注意可能有当前没有0的情况,要判断一下。
#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N], n;
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &a[i]);
int pos = 0;
_for(i, 1, n)
if(a[i])
{
pos = i - 1;
break;
}
while(n > 1 && a[n - 1])
{
_for(i, max(pos, 1), n) a[i] = a[i + 1] - a[i];
n--;
sort(a + max(pos, 1), a + n + 1);
if(pos) pos--;
while(pos + 1 <= n && !a[pos + 1]) pos++;
}
printf("%d\n", a[n]);
}
return 0;
}
C. Qpwoeirut And The City(括号匹配)
思路挺妙,首先左边问号全部左括号,右边全部右括号一定是一个解
然后看有无第二个解,那就把中间那里交换一下,也就是最右边的左括号和最左边的右括号,这样是影响最小的,然后看这样是否合法。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
bool check(string s)
{
int len = s.size(), cnt = 0;
rep(i, 0, len)
{
if(s[i] == '(') cnt++;
else cnt--;
if(cnt < 0) return false;
}
return true;
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
string s;
cin >> s;
int len = s.size();
vector<int> pos;
int l = len / 2, r = len / 2;
rep(i, 0, len)
{
if(s[i] == '(') l--;
else if(s[i] == ')') r--;
else pos.push_back(i);
}
rep(i, 0, l) s[pos[i]] = '(';
rep(i, l, pos.size()) s[pos[i]] = ')';
int ans = 0;
if(l && r)
{
swap(s[pos[l - 1]], s[pos[l]]);
if(check(s)) ans = 1;
}
puts(ans ? "NO" : "YES");
}
return 0;
}
C. Qpwoeirut And The City(细节)
这题不难,但写的时候写错了一个细节,调了很久才发现
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
ll h[N], a[N];
int n;
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
_for(i, 1, n) scanf("%lld", &h[i]);
_for(i, 2, n - 1) a[i] = max(max(h[i - 1], h[i + 1]) + 1 - h[i], 0ll);
if(n % 2 == 1)
{
ll ans = 0;
for(int i = 2; i <= n; i += 2) ans += a[i];
printf("%lld\n", ans);
continue;
}
vector<pair<ll, ll>> ve;
for(int i = 2; i + 1 <= n; i += 2) ve.push_back({a[i], a[i + 1]});
//这里两个两个打进去时,注意退出条件是i+1<=n 不是i <= n 要用这一块的终点而不是起点
ll cur = 0;
for(auto x: ve) cur += x.second;
ll ans = cur;
for(auto x: ve)
{
cur += x.first - x.second;
ans = min(ans, cur);
}
printf("%lld\n", ans);
}
return 0;
}
Yet Another FFT Problem?(复杂度计算)
这题的关键在于,O(n^2)的算法看起来会T,实际上不会T
先判断a,b中有没有相等的,都有的话直接输出答案,否则去重,之后就暴力枚举a,b
因为要输出下标,我开始的做法是将值和下标绑在一起用pair,但是这种方法写起来复杂,而且交上去MLE了,应该是vector里面直接存下标即可。
学了学菜狗的代码,学到了很多
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e7 + 10;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
vector<int> a(n + 1), b(m + 1);
_for(i, 1, n) cin >> a[i];
_for(i, 1, m) cin >> b[i];
vector<int> posa(N, -1), posb(N, -1), va, vb;
array<int, 4> ans = {-1, -1, -1, -1};
_for(i, 1, n)
{
if(posa[a[i]] == -1)
{
posa[a[i]] = i;
va.push_back(i);
}
else
{
ans[0] = posa[a[i]];
ans[1] = i;
}
}
_for(i, 1, m)
{
if(posb[b[i]] == -1)
{
posb[b[i]] = i;
vb.push_back(i);
}
else
{
ans[2] = posb[b[i]];
ans[3] = i;
}
}
if(ans[0] != -1 && ans[2] != -1)
{
cout << ans[0] << " " << ans[1] << " " << ans[2] << " " << ans[3] << "\n";
return 0;
}
vector<array<int, 2>> vis(2 * N, {-1, -1});
for(auto i : va)
for(auto j: vb)
{
if(vis[a[i] + b[j]][0] == -1) vis[a[i] + b[j]] = {i, j};
else
{
cout << i << " " << vis[a[i] + b[j]][0] << " " << j << " " << vis[a[i] + b[j]][1];
return 0;
}
}
cout << "-1\n" ;
return 0;
}
Good red-string(贪心)
这道题是比较复杂的贪心,思维量比较大
首先不能直接从左到右先填r,再填e,再填e,比如r?d??? 先填r的话就会导致在前缀中d比e多
因此我们要先处理这种特殊情况,再贪心
首先对所有前缀需要满足cntr >= cnte >= cntd
对于所有后缀需要满足cntd >= cnte >= cntr
结合那个特例,可以化简成对前缀需要d >= e 对后缀需要e >= r 满足这两个条件即满足上面的式子
因为是对称的,所以我们只用考虑前缀,后缀类似处理即可。
对于前缀,统计e的数量和d数量,如果不满足d比e多,那就把最近一个问号填为e,显然e越靠中间越优,所以是最近的。后缀同理填e
这样填完后再进行r e d的贪心,最后再判断是否合法。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
bool solve()
{
string s; cin >> s;
int len = s.size();
if(len % 3) return false;
vector<int> pos;
int cnte = 0, cntd = 0, cntr = 0;
rep(i, 0, len)
{
if(s[i] == 'e') cnte++;
else if(s[i] == 'd') cntd++;
else if(s[i] == '?') pos.push_back(i);
if(cnte < cntd)
{
if(!pos.size()) return false;
s[pos.back()] = 'e'; pos.pop_back();
cnte++;
}
}
cntr = cnte = cntd = 0;
pos.clear();
for(int i = len - 1; i >= 0; i--)
{
if(s[i] == 'e') cnte++;
else if(s[i] == 'r') cntr++;
else if(s[i] == '?') pos.push_back(i);
else cntd++;
if(cnte < cntr)
{
if(!pos.size()) return false;
s[pos.back()] = 'e'; pos.pop_back();
cnte++;
}
}
int r = len / 3 - cntr, e = len / 3 - cnte, d = len / 3 - cntd;
if(r < 0 || e < 0 || d < 0) return false;
rep(i, 0, len)
if(s[i] == '?')
{
if(r) s[i] = 'r', r--;
else if(e) s[i] = 'e', e--;
else if(d) s[i] = 'd', d--;
}
cntr = cntd = cnte = 0;
rep(i, 0, len)
{
if(s[i] == 'r') cntr++;
if(s[i] == 'e') cnte++;
if(s[i] == 'd') cntd++;
if(!(cntr >= cnte && cnte >= cntd)) return false;
}
cntr = cntd = cnte = 0;
for(int i = len - 1; i >= 0; i--)
{
if(s[i] == 'r') cntr++;
if(s[i] == 'e') cnte++;
if(s[i] == 'd') cntd++;
if(!(cntd >= cnte && cnte >= cntr)) return false;
}
return true;
}
int main()
{
int T; scanf("%d", &T);
while(T--) puts(solve() ? "Yes" : "No");
return 0;
}
周三
poj 2287(贪心+分类讨论)
比较需要思考的贪心,挺有趣的
先看两边最高的
首先如果左边最高的大于右边最高的,那就比一场。显然要以最小代价赢,这样赢得话两边得差是最小的,比较划算
如果左边最高的小于右边最高的,那么这样左边怎么样都赢不了,那就拿左边最差的去送,代价最小
如果左边最高的等于右边最高的,那就需要讨论一下
这时有两种方法,一种是最高的比一个平局,一个是拿左边最差的去比。
那么就需要讨论左边最差的那个有没有价值。
如果左边最差的小于右边最差的:如果最高的比一个平局,那么就是一个平局,最差的输了,减200。如果拿左边最差的去比,输200,然而左边最好的可能大于右边第二好的。所以第二种要更优秀。
如果左边最差的等于右边最差的,那么这时其实两种策略都是平手,都可以。
如果左边最差的强于右边最差的。如果第一种策略,那就是平局加上赢一场,加200。如果第二种策略,那就是输一场,然后左边最高的可能赢右边第二高的。第一种策略必为+200,第二种可能+200,选第一种。
所以综合一下上述的选择,可以发现就是最好的比一下,如果能赢就这么比,否则看最差的比一下,如果能赢就比,否则就最差的去比最好的。
其实就是分类讨论,然后分析那种策略最优,然后总结
#include <cstdio>
#include <algorithm>
#include <functional>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e3 + 10;
int a[N], b[N], n;
int main()
{
while(scanf("%d", &n) && n)
{
_for(i, 1, n) scanf("%d", &a[i]);
_for(i, 1, n) scanf("%d", &b[i]);
sort(a + 1, a + n + 1, greater<int>());
sort(b + 1, b + n + 1, greater<int>());
int al = 1, ar = n, bl = 1, br = n, ans = 0;
while(al <= ar)
{
if(a[al] > b[bl])
{
ans += 200;
al++; bl++;
}
else if(a[ar] > b[br])
{
ans += 200;
ar--; br--;
}
else
{
if(a[ar] < b[bl]) ans -= 200;
ar--; bl++;
}
}
printf("%d\n", ans);
}
return 0;
}
hdu 6534(莫队+树状数组+离散化)
因为数据范围比较小,所以可以考虑莫队。
我离散化那里卡了一下,因为不仅要把a[i]离散化,还有差为k这个限制
比较好写的做法是将a[i]-k,a[i]+k同样扔到离散化数组里面,这样就很好处理了
#include <cstdio>
#include <algorithm>
#include <functional>
#include <cmath>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 27000 + 10;
int a[N], L[N], R[N], lsh[N * 3], cnt, n, m, k, block, t;
struct query
{
int l, r, bl, id;
}q[N];
ll ans[N], f[N * 3], sum;
bool cmp(query x, query y)
{
if(x.bl != y.bl) return x.bl < y.bl;
if(x.bl & 1) return x.r < y.r;
return x.r > y.r;
}
int lowbit(int x) { return x & -x; }
void modify(int x, int p)
{
for(; x <= t; x += lowbit(x))
f[x] += p;
}
ll s(int x)
{
ll res = 0;
for(; x; x -= lowbit(x))
res += f[x];
return res;
}
void add(int x)
{
sum += s(R[x]) - s(L[x] - 1);
modify(a[x], 1);
}
void erase(int x)
{
modify(a[x], -1);
sum -= s(R[x]) - s(L[x] - 1);
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
_for(i, 1, n)
{
scanf("%d", &a[i]);
lsh[++cnt] = a[i];
lsh[++cnt] = a[i] - k;
lsh[++cnt] = a[i] + k;
}
sort(lsh + 1, lsh + cnt + 1);
t = unique(lsh + 1, lsh + cnt + 1) - lsh - 1;
_for(i, 1, n)
{
L[i] = lower_bound(lsh + 1, lsh + t + 1, a[i] - k) - lsh;
R[i] = lower_bound(lsh + 1, lsh + t + 1, a[i] + k) - lsh;
a[i] = lower_bound(lsh + 1, lsh + t + 1, a[i]) - lsh;
}
block = sqrt(n);
_for(i, 1, m)
{
int l, r; scanf("%d%d", &l, &r);
q[i] = {l, r, l / block, i};
}
sort(q + 1, q + m + 1, cmp);
int l = 1, r = 0;
_for(i, 1, m)
{
int ll = q[i].l, rr = q[i].r;
while(l < ll) erase(l++);
while(l > ll) add(--l);
while(r < rr) add(++r);
while(r > rr) erase(r--);
ans[q[i].id] = sum;
}
_for(i, 1, m) printf("%lld\n", ans[i]);
return 0;
}
Chiitoitsu(期望dp)
最优策略就是能配对的配对,否则仍掉
以手上的单牌和剩下牌的总数作为状态,用期望dp
注意边界情况
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
ll dp[20][150];
ll binpow(ll a, ll b)
{
ll res = 1;
for(; b; b >>= 1)
{
if(b & 1) res = res * a % mod;
a = a * a % mod;
}
return res;
}
ll inv(ll x) { return binpow(x, mod - 2); }
int main()
{
_for(i, 1, 13)
_for(j, 3 * i, 123)
dp[i][j] = (1 + 3 * i * inv(j) % mod * dp[max(i - 2, 0)][j - 1] % mod
+ (j - 3 * i) * inv(j) % mod * dp[i][j - 1] % mod) % mod;
int T, kase = 0;
scanf("%d", &T);
while(T--)
{
string s; cin >> s;
int len = s.size();
map<string, int> mp;
for(int i = 0; i < len; i += 2)
mp[s.substr(i, 2)]++;
int cnt = 0;
for(auto x: mp)
if(x.second == 1)
cnt++;
printf("Case #%d: %lld\n", ++kase, dp[cnt][123]);
}
return 0;
}
B. Mainak and Interesting Sequence
比赛时当n为偶数,m为奇数的时候,构造了一下,发现构造不出来,就猜了一个肯定不成立,然后直接交了,A了。
赛后可以证明一下,首先除了最大的数,其他的数都是成对的
首先如果全部为一个数,那么n为偶数,和肯定为偶数,而m为奇数,排除
那么不是全部为一个数,除了最大数外,其他的数因为是成对的,所以和为偶数,那么剩下最大数的位置也是偶数的,那么最后的和为偶数,不可能m为奇数
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
int a[N], n, m;
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d%d", &n, &m);
if(n % 2 == 1)
{
if(n > m) puts("No");
else
{
_for(i, 1, n - 1) a[i] = 1, m--;
a[n] = m;
puts("Yes");
_for(i, 1, n) printf("%d ", a[i]); puts("");
}
}
else
{
if(m % 2 == 1) puts("No");
else
{
if(n > m) puts("No");
else
{
_for(i, 1, n - 2) a[i] = 1, m--;
a[n - 1] = a[n] = m / 2;
puts("Yes");
_for(i, 1, n) printf("%d ", a[i]); puts("");
}
}
}
}
return 0;
}
C. Jatayu's Balanced Bracket Sequence
有两种边,一种是括号匹配的,一种是匹配与匹配之间的。比赛时用并查集写的,实际上并不会成环,可以直接统计右括号和)(的个数。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 2e5 + 10;
int f[N], st[N], top, n, cnt;
int find(int x) { return f[x] == x ? x : f[x] = find(f[x]); }
void merge(int x, int y)
{
int fx = find(x), fy = find(y);
if(fx != fy)
{
cnt--;
f[fx] = fy;
}
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
_for(i, 1, 2 * n) f[i] = i;
top = 0;
cnt = 2 * n;
string s; cin >> s;
s = " " + s;
int pre;
_for(i, 1, 2 * n)
{
if(s[i] == '(')
{
if(s[i - 1] == ')') merge(i, pre);
st[++top] = i;
}
else
{
pre = i;
merge(i, st[top--]);
}
}
printf("%d\n", cnt);
}
return 0;
}
D. Edge Split
这题等相通了发现不难,但比赛时还是想了一个小时,很不应该。
显然可以生成树一种颜色,非树边一种颜色。当非数边成环时,将一条边加到生成树中,生成树新成的环删去一条边即可
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 2e5 + 10;
vector<pair<int, int>> g[N];
int ans[N], vis[N], d[N], f[N], n, m;
vector<array<int, 3>> edge;
void dfs(int u, int fa)
{
d[u] = d[fa] + 1;
vis[u] = 1;
for(auto x: g[u])
{
int v = x.first, id = x.second;
if(v == fa)
{
f[u] = id;
continue;
}
if(vis[v])
{
int flag = 1;
for(auto x: edge)
if(x[2] == id)
{
flag = 0;
break;
}
if(flag) edge.push_back({u, v, id});
}
else dfs(v, u);
}
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d%d", &n, &m);
edge.clear();
_for(i, 1, n) g[i].clear(), vis[i] = 0;
_for(i, 1, m) ans[i] = 0;
_for(i, 1, m)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back({v, i});
g[v].push_back({u, i});
}
dfs(1, 0);
int flag = 0;
if(edge.size() == 3)
{
set<int> s;
for(auto x: edge)
{
s.insert(x[0]);
s.insert(x[1]);
}
if(s.size() == 3) flag = 1;
}
if(!flag) for(auto x: edge) ans[x[2]] = 1;
else
{
ans[edge[0][2]] = ans[edge[1][2]] = 1;
int u = edge[2][0], v = edge[2][1];
if(d[u] < d[v]) swap(u, v);
ans[f[u]] = 1;
}
_for(i, 1, m) printf("%d", ans[i]); puts("");
}
return 0;
}
A2. Burenka and Traditions (hard version)(异或+贪心区间覆盖)
这题是由三个关键点嵌套起来。
1.花费为区间除以2向上取整
任何一个操作的花费都可以拆分成长度为1和长度为2的操作,这样考虑一定包含的最优的答案。这样拆分可以简化问题
这样子答案的上界就是n,每个数都区间长度为1的操作
为了让花费最小,显然要尽可能多的操作区间为2的且一起为0,也就是相等。
考虑对于ai-1 和ai 如果本来就不等,那么如果使它们相等呢。在ai-1之前可以一直操作区间为2的,比如说a6,它可以通过前面的操作变成a6^a5,a6^a5^a4,a6^a5^a4^a3……
那么怎么判断是否可以使得相等呢,这就用到了异或的性质,是第二个关键点
2.异或的性质
直接判断比较难,我们加入前缀异或和,也就是从1到ai-1,记为sum
那么也就是存在前缀异或和为sum ^ a[i],注意这个前缀异或和包括0
也就是对于一个ai,可以判断可能存在一个区间,使得答案更小。
那么就由很多个区间,就转化为区间覆盖问题
3.贪心求区间覆盖
结论是按照右端点排序,然后遍历,能放就放
因为可以按照右端点排序,所以可以直接枚举a1到an,然后如果当前可以使答案更小,那就操作,操作之后前缀异或和要重新记录,注意0始终存在。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
int a[N], n;
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &a[i]);
set<int> s;
s.insert(0);
int ans = n, sum = 0;
_for(i, 1, n)
{
if(s.count(sum ^ a[i]))
{
ans--;
s.clear(); s.insert(0);
sum = 0;
}
else sum ^= a[i], s.insert(sum);
}
printf("%d\n", ans);
}
return 0;
}
周四
D. Madoka and The Corruption Scheme(思维+组合数)
这题的关键在于反过来看,我一直按照题目的意思来看,结果感觉比较复杂。
逆向思考,其实就是从根节点,每次选一条红边往下到叶子,叶子就是胜者。
那么显然,要改变的话,只有改变这条路径上的边才有用,而且不同改变的结果一定是不同的,可以看作线段树向下,每次向左边或者向右边,只要有一次不同,结果一定不同。
考虑改变能产生的不同的结果,修改0次就是c(n, 0) 修改1次就是c(n,1)……
令k=min(k, n) 那么结果一共有c(n, 0) + c(n,1)……c(n, k)这么多可能
那改变的人肯定取这么多可能的最大值,那么布置的人一定是布置1,2,3……这样可以使得最大值最小。
所以最终的答案就是这个和
预处理阶乘即可
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
const int mod = 1e9 + 7;
ll f[N];
ll binpow(ll a, ll b)
{
ll res = 1;
for(; b; b >>= 1)
{
if(b & 1) res = res * a % mod;
a = a * a % mod;
}
return res;
}
ll inv(ll x) { return binpow(x, mod - 2); }
ll C(ll n, ll m)
{
return f[n] * inv(f[m]) % mod * inv(f[n - m]) % mod;
}
int main()
{
f[0] = 1;
_for(i, 1, 1e5) f[i] = f[i - 1] * i % mod;
int n, k;
scanf("%d%d", &n, &k);
k = min(k, n);
ll ans = 0;
_for(i, 0, k) ans = (ans + C(n, i)) % mod;
printf("%lld\n", ans);
return 0;
}
Build a Tree and That Is It(思维+解方程)
可以发现只有两种情况,要不是链式,要不是4号为根,然后1 2 3连向4
第一种情况很好处理,第二种情况需要解方程。如果解出小数或者小于等于0就无解。
最后还要注意一下点数和边数要满足限制。
写的时候有想到,但是以为不会超,但实际上是会超的,WA了一发。其实写了也没错,应该一开始就写上去。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
int n, d12, d23, d13, cnt, root;
vector<pair<int, int>> ans;
int dis[5][5];
void add(int u, int x)
{
vector<int> ve;
ve.push_back(u);
_for(i, 1, x - 1) ve.push_back(cnt++);
ve.push_back(root);
rep(i, 0, ve.size() - 1) ans.push_back({ve[i], ve[i + 1]});
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
cnt = 4;
ans.clear();
scanf("%d%d%d%d", &n, &d12, &d23, &d13);
dis[1][2] = dis[2][1] = d12;
dis[2][3] = dis[3][2] = d23;
dis[1][3] = dis[3][1] = d13;
//case 1
vector<pair<int, int>> ve;
ve.push_back({d12, 3});
ve.push_back({d23, 1});
ve.push_back({d13, 2});
sort(ve.begin(), ve.end());
if(ve[0].first + ve[1].first == ve[2].first)
{
root = ve[2].second;
_for(i, 1, 3)
if(i != root)
add(i, dis[i][root]);
}
else //case 2
{
if((d13 + d12 - d23) % 2 != 0)
{
puts("NO");
continue;
}
int a, b, c;
a = (d13 + d12 - d23) / 2;
c = d13 - a;
b = d23 - c;
if(a <= 0 || b <= 0 || c <= 0)
{
puts("NO");
continue;
}
root = cnt++;
if(root > n)
{
puts("NO");
continue;
}
add(1, a); add(2, b); add(3, c);
}
while(cnt <= n) ans.push_back({cnt++, root});
if(cnt > n + 1) puts("NO");
else
{
puts("YES");
for(auto [u, v]: ans) printf("%d %d\n", u, v);
}
}
return 0;
}
D. Magical Array(思维)
妙啊,我自己思考的时候就觉得操作1和操作2会使得序列的某个性质改变,通过这个性质判断
一开始想的是和,但是都是使得和不变的
突破口是操作2的下标不同,所以我们把下标加入到序列的性质中,也就是ai * i的求和,可以发现操作2使得这个值+1,操作一不变,通过这个即可得出答案
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
vector<pair<ll, int>> ve;
int main()
{
int T; scanf("%d", &T);
while(T--)
{
ve.clear();
int n, m;
scanf("%d%d", &n, &m);
_for(i, 1, n)
{
ll sum = 0;
_for(j, 1, m)
{
ll x; scanf("%lld", &x);
sum += x * j;
}
ve.push_back({sum, i});
}
sort(ve.begin(), ve.end());
printf("%d %lld\n", ve.back().second, ve.back().first - ve[0].first);
}
return 0;
}
G1. Passable Paths(dfs)
我的思路是找所有点的lca,然后从这开始搜,看经过这一点的路径最大值是否为k。
题目的思路简单一些,找深度最大的节点dfs,这个节点一定是路径的一端,然后后dfs能否走过所有选择的点
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 2e5 + 10;
vector<int> g[N];
int d[N], up[N][21], c[N], n, q, k, ans, root;
void dfs(int u, int fa)
{
d[u] = d[fa] + 1;
up[u][0] = fa;
_for(j, 1, 20) up[u][j] = up[up[u][j - 1]][j - 1];
for(int v: g[u])
{
if(v == fa) continue;
dfs(v, u);
}
}
int lca(int u, int v)
{
if(d[u] < d[v]) swap(u, v);
for(int j = 20; j >= 0; j--)
if(d[up[u][j]] >= d[v])
u = up[u][j];
if(u == v) return u;
for(int j = 20; j >= 0; j--)
if(up[u][j] != up[v][j])
u = up[u][j], v = up[v][j];
return up[u][0];
}
int dfs2(int u, int fa)
{
int mx1 = 0, mx2 = 0;
for(int v: g[u])
{
if(v == fa) continue;
int t = dfs2(v, u);
if(t > mx1) mx2 = mx1, mx1 = t;
else mx2 = max(mx2, t);
}
if(u == root && c[u] + mx1 + mx2 == k) ans = 1;
return c[u] + mx1;
}
int main()
{
scanf("%d", &n);
_for(i, 1, n) g[i].clear();
_for(i, 1, n - 1)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 0);
scanf("%d", &q);
_for(i, 1, q)
{
root = 0;
_for(i, 1, n) c[i] = 0;
scanf("%d", &k);
_for(j, 1, k)
{
int x; scanf("%d", &x);
c[x] = 1;
if(!root) root = x;
else root = lca(root, x);
}
ans = 0;
dfs2(root, 0);
puts(ans ? "YES" : "NO");
}
return 0;
}
D. Permutation Restoration(贪心)
首先可以算出一个可能的取值范围,注意当分数小于一个值,转化为开区间可以向下取整+1
然后就变成了怎么给每个区间分配点,我在这里卡住了,想把区间排序,然后不太行。
正解是反过来思考,给点分配区间,从1遍历到n,对于当前点,给它分配一个右端点最小的区间。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 5e5 + 10;
vector<int> v[N];
int b[N], a[N], n;
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
_for(i, 1, n) v[i].clear();
_for(i, 1, n)
{
scanf("%d", &b[i]);
int l = i / (b[i] + 1) + 1;
v[l].push_back(i);
}
set<pair<int, int>> s;
_for(i, 1, n)
{
for(int id: v[i]) s.insert({(b[id] == 0) ? n : id / b[id], id});
a[s.begin()->second] = i;
s.erase(s.begin());
}
_for(i, 1, n) printf("%d ", a[i]); puts("");
}
return 0;
}
周五
D. Guess The String(交互题+二分)
交互题做的非常少……
这题提供的主要信息就是一段区间右多少种字符
考虑一个一个字符来猜,如果前面字符已知,现在猜第i个字符
考虑f(j, i) 和f(j, i-1) f(x,y)表示[x, y]中有多少种字符
f(j, i) 和f(j, i-1)的差别就是多了一个s[i]
显然,如果s[i]在(j,i-1)未出现过,那么f(j, i)会大1,否则相等
当j离i越近,(j,i-1)就越小,越难包含s[i],如果j离i越远,那么就越可能包含s[i]
考虑f(j, i) - f(j, i - 1) 那么随着j的增大,它的值一定是0 0 0 0 0 1 1 1 1
最右边的0的位置就是s[i]
因为0011具有单调性,所以我们可以二分来找这个s[i]
当然,右可能前面每出现过找不到,此时就询问i
那么这种做法询问是比较多的,因为有log(1000) 6000的数据范围意味着每个i最多6次
考虑优化,显然只有每个字母最后出现的位置是有意义的,那么我们可以只存这个,那么二分次数就大大减小。
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e3 + 10;
int pos[30], cnt, n;
char ans[N];
char ask1(int x)
{
printf("? 1 %d\n", x);
fflush(stdout);
string s; cin >> s; //输入一个字符时要额外小心 应该这么写
return s[0];
}
int ask2(int l, int r)
{
printf("? 2 %d %d\n", l, r);
fflush(stdout);
int t; scanf("%d", &t);
return t;
}
int main()
{
scanf("%d", &n);
ans[1] = ask1(1);
pos[++cnt] = 1;
_for(i, 1, n)
{
sort(pos + 1, pos + cnt + 1);
int l = 0, r = cnt + 1;
while(l + 1 < r)
{
int m = l + r >> 1;
if(ask2(pos[m], i) == (cnt - m + 1)) l = m;
else r = m;
}
if(l == 0)
{
ans[i] = ask1(i);
pos[++cnt] = i;
}
else
{
ans[i] = ans[pos[l]];
pos[l] = i;
}
}
printf("! ");
_for(i, 1, n) putchar(ans[i]);
fflush(stdout);
return 0;
}
D. Permutation Graph(单调栈+线段树)
这题妙啊,有两种做法。
第一种做法比较直接,但写起来复杂一些,第二种不太好想到,但是写起来简单
第一种做法
首先,有一个贪心猜测,每一次走尽可能的远。我一开始有这个想法,但不知道这个贪心是不是对的。其实很好证明,如果u最远能达到v,那么u和v中间的节点一定不能连一条边超过v。因为如果可以,比如中间有个节点a,连一条边到v右边的b,那么可以发现u是可以连边到b的,就不满足v是最远的这个前提。
有这个结论后,考虑怎么迅速求这个最远的点,比如当前节点是i,那么a[i]与a[i+1]的大小关系显然决定了a[i]是最大值还是最小值,如果a[i] > a[i+1]那么显然a[i]要作为最大值
如果a[i]是作为最大值的话,设j为离i最近的使得a[j] > a[i],那么可能得答案在[i + 1, j - 1]中
这个j可以用单调栈O(n)求出
在可能的答案区间中,求最小值,这个最小值就是我们要的答案。因为不可能有边从i越过这个最小值,因为i已经是最小值了。所以可以用线段树预处理区间最值,同时存对应的下标(用pair写起来非常简洁),那么就可以迅速求出
每次跳一次的复杂度是线段树的O(logn),而每次至少可以往右移动一个,那么最多跳n次,所以复杂度是O(nlogn)的
#include <bits/stdc++.h>
#define l(k) (k << 1)
#define r(k) (k << 1 | 1)
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef pair<int, int> pa;
const int N = 2.5e5 + 10;
int a[N], rmax[N], rmin[N], st[N], top, n;
pa tmax[N << 2], tmin[N << 2];
void up(int k)
{
tmax[k] = max(tmax[l(k)], tmax[r(k)]);
tmin[k] = min(tmin[l(k)], tmin[r(k)]);
}
void build(int k, int l, int r)
{
if(l == r)
{
tmax[k] = tmin[k] = {a[l], l};
return;
}
int m = l + r >> 1;
build(l(k), l, m);
build(r(k), m + 1, r);
up(k);
}
pa ask_max(int k, int l, int r, int L, int R)
{
if(L <= l && r <= R) return tmax[k];
int m = l + r >> 1;
pa res = {0, 0};
if(L <= m) res = max(res, ask_max(l(k), l, m, L, R));
if(R > m) res = max(res, ask_max(r(k), m + 1, r, L, R));
return res;
}
pa ask_min(int k, int l, int r, int L, int R)
{
if(L <= l && r <= R) return tmin[k];
int m = l + r >> 1;
pa res = {1e9, 0};
if(L <= m) res = min(res, ask_min(l(k), l, m, L, R));
if(R > m) res = min(res, ask_min(r(k), m + 1, r, L, R));
return res;
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &a[i]);
top = 0;
for(int i = n; i >= 1; i--) //单调栈不用先加入 直接从n开始
{
while(top && a[i] > a[st[top]]) top--;
rmax[i] = top ? st[top] : n + 1; //注意栈未空的情况
st[++top] = i;
}
top = 0;
for(int i = n; i >= 1; i--)
{
while(top && a[i] < a[st[top]]) top--;
rmin[i] = top ? st[top] : n + 1;
st[++top] = i;
}
build(1, 1, n);
int ans = 0, cur = 1;
while(cur != n)
{
if(a[cur + 1] > a[cur])
{
pa t = ask_max(1, 1, n, cur + 1, rmin[cur] - 1);
cur = t.second;
}
else
{
pa t = ask_min(1, 1, n, cur + 1, rmax[cur] - 1);
cur = t.second;
}
ans++;
}
printf("%d\n", ans);
}
return 0;
}
第二种做法
核心是一个点:不会有边跨过最值
对于ai=n,一定不会有边跨过它,对于aj=1也是
不能跨国它意味着一定要经过它
同时i和j存在一条边
假设i<j
所以dis(1, n) = dis(1, i) + 1 + dis(j, n)
那么可以这样一直递归下去,只不过最值变成了当前区间的最值
这里的区间要不是l=1,要不是r=n,所以只需要处理前缀最值和后缀最值
还要存值对应的下标
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef pair<int, int> pa;
const int N = 2.5e5 + 10;
int a[N], lmax[N], lmin[N], rmax[N], rmin[N], id[N], n;
int dfs(int l, int r)
{
if(l == r) return 0;
int x, y;
if(l == 1)
{
x = min(id[lmax[r]], id[lmin[r]]);
y = max(id[lmax[r]], id[lmin[r]]);
}
else
{
x = min(id[rmax[l]], id[rmin[l]]);
y = max(id[rmax[l]], id[rmin[l]]);
}
return dfs(l, x) + 1 + dfs(y, r);
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
_for(i, 1, n) scanf("%d", &a[i]), id[a[i]] = i;
lmax[1] = lmin[1] = a[1];
_for(i, 2, n)
{
lmax[i] = max(lmax[i - 1], a[i]);
lmin[i] = min(lmin[i - 1], a[i]);
}
rmax[n] = rmin[n] = a[n];
for(int i = n - 1; i >= 1; i--)
{
rmax[i] = max(rmax[i + 1], a[i]);
rmin[i] = min(rmin[i + 1], a[i]);
}
printf("%d\n", dfs(1, n));
}
return 0;
}
D. Lena and Matrix(拆式子)
这题的关键在于把题目给的式子变换,拆开
分类一下把那个绝对值拆开,发现最值一定是4个点之一,所以有用的只有4个点
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e3 + 10;
char s[N][N];
int n, m;
int main()
{
int T; scanf("%d", &T);
while(T--)
{
vector<pair<int, int>> node(4, {-1, -1});
scanf("%d%d", &n, &m);
_for(i, 1, n)
{
scanf("%s", s[i] + 1);
_for(j, 1, m)
{
if(s[i][j] == 'W') continue;
if(node[0].first == -1)
{
rep(k, 0, 4)
node[k] = {i, j};
}
else
{
if(i + j > node[0].first + node[0].second) node[0] = {i, j};
if(i + j < node[1].first + node[1].second) node[1] = {i, j};
if(i - j > node[2].first - node[2].second) node[2] = {i, j};
if(i - j < node[3].first - node[3].second) node[3] = {i, j};
}
}
}
int ans = 1e9, x, y;
_for(i, 1, n)
_for(j, 1, m)
{
int cur = 0;
for(auto [xx, yy]: node)
cur = max(cur, abs(xx - i) + abs(yy - j));
if(cur < ans)
{
ans = cur;
x = i; y = j;
}
}
printf("%d %d\n", x, y);
}
return 0;
}