文章目录
前言
距离沈阳站小半年了,一直没写总结。
今年的第一场 i c p c icpc icpc 区域赛 , 只能说, 心情很复杂。
比赛链接:https://ac.nowcoder.com/acm/contest/24346
补补题先吧。
题目一览
签到题:B, E, F, J
铜牌题:无
银牌题:H, I, L, M
金牌题: G
很难很难的题:A,C,D,K
由于没有传统意义上的铜牌题以及银牌题太多 , 导致牌子的分布比较离谱,罚时很重要。
本场差不多是 4.5 题铜,5.5题银,7.5题金
E.Edward Gaming, the Champion(签到)
题意:
给你一个字符串 s s s,问有几个" e d g n b edgnb edgnb"的子串 。 ( 1 < = l e n < = 2 ∗ 1 0 5 ) (1<=len<=2*10^5) (1<=len<=2∗105)
思路:
随便吧
F.Encoded Strings I(签到)
题意:
给你一个长度为n的字符串s ,要对s进行解码。
解码的规则是:
当前字符 " s i = ( s i 最 后 一 次 出 现 后 不 同 字 符 的 数 量 + " a " ) s_i = (s_i最后一次出现后不同字符的数量 + "a") si=(si最后一次出现后不同字符的数量+"a") "。
例如,对于 s s s = “ a b a c d c abacdc abacdc” ,
a a a 最后一次出现后,还有 “ c d c cdc cdc" , 2 2 2个不同字符 , 那么所有的 a a a 被替换成 c c c 。
问对 s s s 的每个前缀,输出解码后字典序最小的。
思路:
$n = 1000 , O(n^2) $的。
那题目让干啥干啥就行了。
B.Bitwise Exclusive-OR Sequence(数学+图DFS)
哎,就是这个题
题意:
给出 n n n个点 , m m m个关系 ,每个关系是 < u , v , w > <u,v,w> <u,v,w>
意思是 点 u u u 异或 点 v v v 结果是 w w w。
要求你给每个点赋值并满足上述所有的关系。
问最小的点权和是多少?
( 1 < = n < = 1 0 5 , 0 < = m < = 2 ∗ 1 0 5 , 0 < = w < 2 30 ) ( 1<=n<=10^5 , 0<=m<=2*10^5 , 0<=w<2^{30}) (1<=n<=105,0<=m<=2∗105,0<=w<230)
思路:
首先对于二进制数来讲, 异或操作是不会影响不同数位的,所以每个二进制位单独出来看 , 对于每一位,我们都尽可能让 ‘‘ 1 1 1’’ 更少。
然后异或其实就是规定了两个点在这一数位应该相同还是不同。
所以把规定了关系的点连一起,由于‘ 0 0 0’ 的数量和 ‘ 1 1 1’的数量是互补的,每个连通分量跑一遍图就好。复杂度 O ( 32 ∗ m ) O(32*m) O(32∗m)
这题我们队被卡常了,场上调了一个多钟, T + W A T+WA T+WA 了 5 5 5发才过 , 直接导致沈阳站凉凉。
我们最后把
#define int long long
去掉就过了。
后来才知道 l o n g l o n g long long longlong 在部分评测机上跑得会比 i n t int int 慢一些 , 大概 1 、 200 m s 1、200ms 1、200ms这样 , 所以开 l o n g l o n g long long longlong的时候要谨慎。
其实一个很重要的原因是我们队一直没有注意到卡常的情况, 导致代码本身写得比较冗杂,要不然 l o n g l o n g long long longlong其实也过得去。
但无论如何 ,还是希望少一点卡常题 , 尤其是重要比赛上…
代码:
重写了一遍,这次只有不到200ms了
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+100;
struct E{
int to;
int nxt;
int w;
}e[N<<1];
int n,m;
int head[N],tot;
int one[32];int siz;
int ans[32];
bool a[32][N];
bool vis[N];
void add_edge(int u,int v,int w){
e[++tot].nxt = head[u];
e[tot].to = v;
e[tot].w = w;
head[u] = tot;
}
bool ok = true;
void dfs(int u,int f){
if(!ok) return;
siz++;
vis[u] = true;
for(int i=head[u];i;i=e[i].nxt){
int v = e[i].to;
if(v==f) continue;
int w = e[i].w;
if(vis[v]){
for(int j=0;j<=30;j++){
bool flg = w&(1<<j);
if(a[j][v]!=(a[j][u]^flg)){ /*把相互矛盾的情况判掉*/
ok = false;
return;
}
}
continue;
}
else{
for(int j=0;j<=30;j++){
bool flg = w&(1<<j);
a[j][v] = a[j][u]^flg;
one[j]+=a[j][v];
}
dfs(v,u);
}
}
}
int fac[30];
int main(){
ios::sync_with_stdio(false);
cin>>n>>m;
fac[0] = 1;
for(int i=1;i<=30;i++) fac[i] = fac[i-1]*2;
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
add_edge(u,v,w);
add_edge(v,u,w);
}
for(int i=1;i<=n;i++){
if(vis[i]) continue;
siz = 0;
for(int j=0;j<=30;j++) one[j] = 0;
dfs(i,0);
for(int j=0;j<=30;j++) ans[j] += min(one[j],siz-one[j]);
}
if(!ok){
cout<<-1<<endl;
return 0;
}
long long sum = 0;
for(int j=0;j<=30;j++){
sum = sum + (long long)ans[j]*(long long)fac[j];
}
cout<<sum<<endl;
}
J.Luggage Lock(思维+BFS)
题意:
给你一个四位的锁,每一位是0~9,就这样的:
每次可以选择一段区间 [ L , R ] [L,R] [L,R] 同时移动一格(上移或下移) , 问经过最少多少次能从状态 a a a 操作到状态 b b b ?
输入
6
1234 2345
1234 0123
1234 2267
1234 3401
1234 1344
1234 2468
输出
1
1
4
5
1
4
思路
把初态变为 0 0 0 ,终态对应着改一下.
然后问题就变成了从 0000 0000 0000到对应状态至少需要几次,BFS打表。
代码
#include<bits/stdc++.h>
using namespace std;
int to[4];
map<string ,int> mp;
map<string ,bool> vis;
string add(string now,int i,int j){
for(int x=i;x<=j;x++){
if(now[x]=='9') now[x] = '0';
else now[x]++;
}
return now;
}
string sub(string now,int i,int j){
for(int x=i;x<=j;x++){
if(now[x]=='0') now[x] = '9';
else now[x]--;
}
return now;
}
void bfs(){
string s = "0000";
mp[s] = 0,vis[s] = true;
queue<string> q;
q.push(s);
while(!q.empty()){
string now = q.front();
q.pop();
for(int i=0;i<4;i++){
for(int j=0;j<=i;j++){
string tmp1 = add(now,j,i);
string tmp2 = sub(now,j,i);
mp[tmp1] = (vis[tmp1]== true)?min(mp[tmp1],mp[now]+1):mp[now]+1;
mp[tmp2] = (vis[tmp2]== true)?min(mp[tmp2],mp[now]+1):mp[now]+1;
if(!vis[tmp1]) q.push(tmp1),vis[tmp1] = true;
if(!vis[tmp2]) q.push(tmp2),vis[tmp2] = true;
}
}
}
}
int main(){
ios::sync_with_stdio(false);
bfs();
int t;
cin>>t;
while(t--){
string a,b;
cin>>a>>b;
string pp;
for(int i=0;i<4;i++){
to[i] = (b[i]-a[i]+10)%10,pp.push_back(to[i]+'0');
}
cout<<mp[pp]<<endl;
}
}
H.Line Graph Matching(图论/tarjan)
题意:
给一个带边权无向图
构造一个新图:将原图的点变成边、边变成点 , 其中新图的边权为原图中对应的俩条边权值和。
求新图中的最大独立边集 — 在新图中选取不相邻的若干条边,让他们的权值和最大。
3 < = n < = 1 0 5 , n − 1 < = m < = 2 ∗ 1 0 5 3<=n<=10^5 , n-1<=m<=2*10^5 3<=n<=105,n−1<=m<=2∗105
输入
5 6
1 2 1
1 3 2
1 4 3
4 3 4
4 5 5
2 5 6
输出
21
样例长这样:
思路:
场上写出来了,但是很可惜, 5 5 5题铜首。
首先很关键的一点是,新图我们是建不出来的。
因为如果原图是菊花图, 那么新图就会是完全图 , 存不下 , 所以只能在原图里找对应关系。
然后不难发现, 其实就是在原图中每次选取两条相邻边,要求最大化权值和
· 如果一共有偶数条边,那么我们是一定能取完所有边的
· 如果一共有奇数条边,那我们必须删掉一条边。如果删的边不是桥的话,那原图就变成偶数条边的连通图,显然可以。
考虑桥的情况:
桥可能会将图变成下面两种 :
1.偶图 - 桥 - 偶图 (第一种桥)
2.奇图 - 桥 - 奇图 (第二种桥)
显然情况2我们是不要的,因为情况2还需要从分出来奇图中再次删边,而很显然,最优解肯定只删一条边(反证一下就行)
所以,奇数条边的答案是 “ s u m − 除 去 第 二 种 桥 外 所 有 的 边 的 最 小 边 权 sum - 除去第二种桥外所有的边的最小边权 sum−除去第二种桥外所有的边的最小边权”
显然我们只需要判断“桥”两端点的size是奇数还是偶数 , 这个tarjan+dfs就可以处理。
代码:
重新写一下,就当复习tarjan。
(比队友的代码长了一倍是怎么回事)
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 6e5+100;
struct E{
int to;
int nxt;
int w;
bool cut;
}e[N<<1];
int head[N],tot=-1;
void add_edge(int u,int v,int w){
e[++tot].to = v;
e[tot].nxt = head[u];
e[tot].w = w;
head[u] = tot;
}
int low[N],dfn[N],idx;
int a[N];
int minn = 1e15+7;
struct edge{
int u;int v;int w;
};
vector<edge> bridge;
void tarjan(int u,int f){
low[u] = dfn[u] = ++idx;
for(int i=head[u];i!=-1;i=e[i].nxt){
int v = e[i].to;
if(v==f) continue;
if(!dfn[v]){
tarjan(v,u);
if(low[u]>low[v]) low[u] = low[v];
if(low[v]>dfn[u]){
e[i].cut = true;
e[i^1].cut = true;
bridge.push_back({u,v,e[i].w});
a[u]--,a[v]--;
}
}
else if(low[u]>dfn[v]){
low[u] = dfn[v];
}
}
}
bool vis[N];
int col[N];
int point[N];
int se;
int cnt;
void dfs(int u,int f){
vis[u] = true;
cnt += a[u];
col[u] = se;
for(int i=head[u];i!=-1;i=e[i].nxt){
int v = e[i].to;
if(vis[v]||v==f||e[i].cut) continue;
dfs(v,u);
}
}
int sz[N];
vector<pair<int,int>> v[N];
void dfs2(int u,int f){
sz[u] = point[u];
for(auto y:v[u]){
int vv = y.first,ww = y.second;
if(vv==f) continue;
dfs2(vv,u);
if(sz[vv]%2==0){
minn = min(minn,ww);
}
sz[u] += sz[vv];
sz[u]++;
}
}
signed main(){
ios::sync_with_stdio(false);
memset(head,-1,sizeof head);
int n,m;
cin>>n>>m;
int sum = 0;
for(int i=1;i<=m;i++){
int x,y,w;
cin>>x>>y>>w;
add_edge(x,y,w);
add_edge(y,x,w);
a[x]++,a[y]++;
sum += w;
}
if(m%2==0) cout<<sum<<endl;
else{
tarjan(1,0);
for(int i=1;i<=n;i++){
if(!vis[i]){
cnt = 0;
se++;
dfs(i,0);
point[se] = cnt/2;
}
}
for(auto j:bridge){
int x = col[j.u],y = col[j.v],z = j.w;
v[x].push_back(make_pair(y,z));
v[y].push_back(make_pair(x,z));
}
dfs2(1,0);
for(int i=0;i<=tot;i++){
if(!e[i].cut) minn = min(minn,e[i].w);
}
cout<<sum-minn<<endl;
}
return 0;
}
L.Perfect Matchings(树形dp+容斥)
题意
给一个 2 n 2n 2n 个点的完全图,从中删去一个 2 n − 1 2n-1 2n−1 个点的树,问剩下的图中有几个完美匹配。
完美匹配是指一个由 n n n 条边组成的边集 , 其中任意两条边不共享同一顶点。
( 2 < = n < = 2000 2<=n<=2000 2<=n<=2000)
思路
考虑容斥 , 令 a n s i ans_i ansi 表示选了 i i i 条树边的方案数。
选了 i i i 条树边, 就是选了 2 i 2i 2i 个点,此时还剩下 ( 2 n − 2 i ) (2n - 2i) (2n−2i) 个点。
对于完美匹配 , 我们从这 ( 2 n − 2 i ) (2n - 2i) (2n−2i) 个点中,选一半放左边 , 一半放右边,左右任意匹配
方案数为: C 2 n − 2 i n − i ∗ ( n − i ) ! 2 n \frac{C_{2n-2i}^{n-i}*(n-i)!}{2^n} 2nC2n−2in−i∗(n−i)!
因此 ,answer = ∑ i = 0 n C 2 n − 2 i n − i ∗ ( n − i ) ! 2 n ∗ a n s i ∗ ( − 1 ) i \sum_{i=0}^n \frac{C_{2n-2i}^{n-i}*(n-i)!}{2^n} *ans_i*(-1)^i ∑i=0n2nC2n−2in−i∗(n−i)!∗ansi∗(−1)i
对于求 a n s i ans_i ansi , 考虑 d p dp dp:
d p [ N ] [ N ] [ 2 ] dp[N][N][2] dp[N][N][2] 表示 当前在 i i i 号点 , 它的子树选了 j j j 条边参加匹配 , 当前点是否参与匹配。
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 4040;
const int mod = 998244353;
struct E{
int to;
int nxt;
}e[N<<1];
int head[N],tot;
int fac[N],inv[N],sz[N],in2[N];
int dp[N][N][2];
int g[N][2];
void add_edge(int u,int v){
e[++tot].to = v;
e[tot].nxt = head[u];
head[u] = tot;
}
int ksm(int a,int b,int p){
int ans = 1;
while(b){
if(b&1) ans = ans*a%p;
a = a*a%p;
b >>= 1;
}
return ans%p;
}
void pre(){
int mx = 4000;
fac[0] = 1,inv[0] = 1,in2[0] = 1;
in2[1] = ksm(2,mod-2,mod);
for(int i=1;i<=mx;i++){
fac[i] = fac[i-1]*i%mod;
inv[i] = ksm(fac[i],mod-2,mod)%mod;
in2[i] = in2[i-1]*in2[1]%mod;
}
}
int C(int n,int m){
if(m>n) return 0;
return fac[n]*inv[n-m]%mod*inv[m]%mod;
}
void dfs(int u,int f){
sz[u] = 1;
dp[u][0][0] = 1;
for(int i=head[u];i;i=e[i].nxt){
int v = e[i].to;
if(v==f) continue;
dfs(v,u);
memset(g,0,sizeof g);
for(int j=0;j<=sz[u]/2;j++){
for(int k=0;k<=sz[v]/2;k++){
g[j+k][0] += dp[u][j][0]*(dp[v][k][0]+dp[v][k][1]);
g[j+k][1] += dp[u][j][1]*(dp[v][k][0]+dp[v][k][1]);
g[j+k+1][1] += dp[u][j][0]*dp[v][k][0];
g[j+k][0] %= mod,g[j+k][1] %= mod,g[j+k+1][1] %= mod;
}
}
sz[u] = sz[u] + sz[v];
for(int j=0;j<=sz[u]/2+1;j++)
dp[u][j][0] = g[j][0],dp[u][j][1] = g[j][1];
}
}
signed main(){
ios::sync_with_stdio(false);
int n;
cin>>n;
pre();
int rt = -1;
for(int i=1;i<=2*n-1;i++){
int u,v;
cin>>u>>v;
rt = u;
add_edge(v,u);
add_edge(u,v);
}
dfs(rt,0);
int sum = 0;
for(int i=0;i<=n;i++){
int now = (dp[rt][i][0]+dp[rt][i][1])%mod;
int res = n-i;
int ans = now * C(res*2,res)%mod*fac[res]%mod*in2[res]%mod;
if(i&1)
sum = (sum - ans%mod+mod)%mod;
else sum = (sum + ans)%mod;
}
cout<<sum<<endl;
}
M.String Problem(字符串/思维+SAM)
题意:
给一个字符串 s s s , 对于它的每个前缀,求其字典序最大的每个子字符串。
( 1 < = s < = 1 0 6 ) (1<=s<=10^6) (1<=s<=106)
输入
potato
输出
1 1
1 2
3 3
3 4
3 5
5 6
输出的是该子字符串的左右端点
思路
首先可以明确的一点是,“最大的子字符串”一定会延申到该字符串的结尾。
那么这道题就有一种很nb的解法 , 用双指针 O ( n ) O(n) O(n)地处理字符信息(至今没看懂)。
考虑后缀自动机 sam…
写博客的时候才发现我已经把sam忘光了,然后回去看了一下午…
样例1的sam:
s a m sam sam已经把每个后缀维护在自动机上了,那我们维护一个 p o s [ ] pos[] pos[],代表该节点在原串中的位置
从起始点开始字典序由大到小 d f s dfs dfs , 某个点第一次被访问到的时候对应的路径肯定就是该点最大的后缀。
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
struct state {
int len, link;
int next[26];
};
const int MAXLEN = 1001000;
state st[MAXLEN<<1];
int sz, last;
void init() {
st[0].len = 0;
st[0].link = -1;
sz++;
last = 0;
}
int pos[MAXLEN<<1];
int ans[MAXLEN<<1];
void extend(int c,int id) {
int cur = sz++;
st[cur].len = st[last].len + 1;
pos[cur] = id;
int p = last;
while (p != -1 && !st[p].next[c]) {
st[p].next[c] = cur;
p = st[p].link;
}
if (p == -1) {
st[cur].link = 0;
} else {
int q = st[p].next[c];
if (st[p].len + 1 == st[q].len) {
st[cur].link = q;
} else {
int clone = sz++;
st[clone].len = st[p].len + 1;
memcpy(st[clone].next,st[q].next,sizeof st[q].next);
st[clone].link = st[q].link;
pos[clone] = pos[q];
while (p != -1 && st[p].next[c] == q) {
st[p].next[c] = clone;
p = st[p].link;
}
st[q].link = st[cur].link = clone;
}
}
last = cur;
}
bool vis[MAXLEN<<1];
void dfs(int u,int len){
vis[u] = true;
for(int c=25;c>=0;c--){
if(st[u].next[c]&&!vis[st[u].next[c]]){
dfs(st[u].next[c],len+1);
}
}
if(!ans[pos[u]]) ans[pos[u]] = pos[u]-len+1;
}
string s;
signed main(){
cin>>s;
init();
for(int i=0;i<s.length();i++){
extend(s[i]-'a',i+1);
}
dfs(0,0);
for(int i=1;i<=s.length();i++)
printf("%d %d\n",ans[i],i);
}
I.Linear Fractional Transformation(数学/高斯消元)
题意
给定一个复变函数 f ( z ) = a ∗ z + b c ∗ z + d f(z) = \frac {a*z+b}{c*z+d} f(z)=c∗z+da∗z+b , 系数待定, a , b , c , d a,b,c,d a,b,c,d 都是实数。
给出3个数 , $f(z_1) = w_1 , f(z_2) = w_2 , f(z_3) = w_3 $
求 f ( z 0 ) f(z_0) f(z0)。
输入多组样例 t ( t < = 1 0 5 ) t (t<=10^5) t(t<=105)
每组样例前三行为 z i z_i zi 的实部虚部 , f ( z i ) f(z_i) f(zi) 的实部虚部。
最后一行为 z 0 z_0 z0 的实部虚部。
输出为 f ( z 0 ) f(z_0) f(z0) 的实部虚部。
输入
2
-1 0 0 -1
0 1 -1 0
1 0 0 1
0 -1
-1 0 -1 0
0 1 0 -1
1 0 1 0
0 -1
输出
1.000000000000000 0.000000000000000
0.000000000000000 1.000000000000000
思路
有四个待定系数 a , b , c , d a,b,c,d a,b,c,d, 给三个方程肯定是求不出来的。
于是根据 c c c 的取值进行分类讨论,
c = 0 : c = 0: c=0:
则 f ( z ) = a d ∗ z + b d f(z) = \frac ad*z+\frac bd f(z)=da∗z+db , 可解。
考虑令 d = ( 1 , 0 ) d = (1,0) d=(1,0) , 即 f ( z ) = a ∗ z + b f(z) = a*z+b f(z)=a∗z+b
带入 ( z 1 , w 1 ) , ( z 2 , w 2 ) (z_1,w_1),(z_2,w_2) (z1,w1),(z2,w2)求出 a , b a,b a,b 。
用 ( z 3 , w 3 ) (z_3,w_3) (z3,w3) 验根 , 如果有 w 3 = a ∗ z 3 + b w_3 = a*z_3+b w3=a∗z3+b ,则 c = 0 c = 0 c=0 成立,直接带入 z 0 z_{0} z0 求解。
c ≠ 0 c \ne 0 c=0 :
题目里给了一句提示:
It can be shown that the answer is always unique to the given contraints.
解唯一 ,那么分子分母对应成比例, 考虑令 c = 1 , 求解出来即可。
令 w = a ∗ z + b z + d w = \frac {a*z+b}{z+d} w=z+da∗z+b
移项得 : a ∗ z + b − d = w ∗ z a*z+b- d = w*z a∗z+b−d=w∗z 。
带入 f ( z 1 ) = w 1 , f ( z 2 ) = w 2 , f ( z 3 ) = w 3 f(z_1) = w_1 , f(z_2) = w_2 , f(z_3) = w_3 f(z1)=w1,f(z2)=w2,f(z3)=w3 得:
注意式子里的 z i , w i z_i,w_i zi,wi 都是复数,板子得相应地改一下。
代码
#include<bits/stdc++.h>
using namespace std;
const double eps = 1e-9;
const int N = 11;
int equ,var; /* 方程数和未知数个数 */
complex<double> a[N][N],x[N];
double ffabs(complex<double> xx){
return xx.imag()*xx.imag()+xx.real()*xx.real();
}
int Gauss(){
int i,j,k,col,max_r;
for(k=0,col=0;k<equ&&col<var;k++,col++){
max_r = k;
for(i=k+1;i<equ;i++){
if(ffabs(a[i][col])> ffabs(a[max_r][col]))
max_r = i;
}
if(ffabs(a[max_r][col])<eps) return 0;
if(k!=max_r){
for(j=col;j<var;j++)
swap(a[k][j],a[max_r][j]);
swap(x[k],x[max_r]);
}
x[k] /= a[k][col];
for(j=col+1;j<var;j++) a[k][j]/=a[k][col];
a[k][col] = 1;
for(i=0;i<equ;i++)
if(i!=k){
x[i] -= x[k]*a[i][col];
for(j=col+1;j<var;j++) a[i][j] -= a[k][j]*a[i][col];
a[i][col] = 0;
}
}
return 1;
}
complex<double> z[4],w[4];
int main(){
int t;
cin>>t;
while(t--){
for(int i=1;i<=3;i++){
double za,zb,wa,wb;
scanf("%lf %lf %lf %lf",&za,&zb,&wa,&wb);
z[i].real(za);z[i].imag(zb);
w[i].real(wa);w[i].imag(wb);
}
double za,zb;
complex<double> ans;
scanf("%lf %lf",&za,&zb);
z[0].real(za),z[0].imag(zb);
complex<double> aa = (w[1]-w[2]) / (z[1]-z[2]);
complex<double> bb = w[1] - aa*z[1];
if(ffabs(aa*z[3]+bb-w[3])<eps){
ans = aa*z[0]+bb;
}
else{
for(int i=0;i<3;i++){
a[i][0] = z[i+1],a[i][1] = complex<double>(1,0),a[i][2] = -w[i+1];
x[i] = w[i+1]*z[i+1];
}
equ = var = 3;
Gauss();
aa = x[0],bb = x[1];
complex<double> cc = complex<double>(1,0),dd = x[2];
ans = (aa*z[0]+bb)/(cc*z[0]+dd);
}
printf("%.10f %.10f\n",ans.real(),ans.imag());
}
return 0;
}