文章目录
写在前面
作为一个ACM萌新,本蒟蒻在今年的牛客和杭电多校中屡遭暴击,主要原因还是自己学的算法太少了,今天也是借着趁热打铁的思想在这里总结一下这两天我认为在我这个阶段比较值得总结的题目。题面就不写出来了,点击标题链接即可访问题目。
牛客多校6A Cake
解题思路
这是一道非常经典的简单博弈+树形dp。观察题面可以发现,不管最后形成的01串如何,Oscar一定会切蛋糕使得他能获得01串前缀中0的最大比例。例如,1100则Oscar会切4块平均的蛋糕,然后获得0.5,Grammy也获得0.5;而011则Oscar只会切一块(也就是一整个蛋糕),获得1,那么Grammy则只能获得0。
接下来考虑从下至上树形dp。当递归到子节点u时, d p u , 0 dp_{u,0} dpu,0代表以该节点的子树最优路径上经过边权为1的边个数, d p u , 1 dp_{u,1} dpu,1代表最优路径的长度。记录这两个信息是为了能够快速计算出状态更新后的比例(如果直接存比例double的话,就不知道路径长度了,信息不够)。在计算的过程中,光有子树的信息是不够的,因此我们需要在dfs时将从根节点到当前节点的路径信息记录下来,用参数的方式传递。
最关键的一步:如何转移!显然,深度为偶数时Grammy选择,深度为奇数时Oscar选择。二人都将在自己选的时候做出最利于自己的选择:也就是Grammy会选择1比例最高的子树,Oscar会选择0比例最高的子树。但在转移的过程中,由于只需考虑前缀中0比例最高的串,因此在当前节点 u u u,搜索子树 v v v时,需要判断之前子树 v v v上的路径0比例与只走到v的路径0比例谁大,选择0比例最大的那一个,用于向上传递更新权值。最后以1为根节点实现dfs即可。时间复杂度 O ( n ) O(n) O(n)或 O ( n log n ) O(n\log n) O(nlogn)
AC代码
其实这道题的思路还是有一点点复杂的,但赛时居然过了一半的队伍,看来大家的水平都很厉害。
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)
#define debug(x) cout << #x << " = " << x << endl
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
const int N = 2e5+7;
map<int,bool> g[N];
int dep[N];
int dp[N], dp2[N];
void dfs0(int u, int fa, int depth) {
dep[u] = depth;
for (auto v: g[u]) {
if (v.first == fa) continue;
dfs0(v.first, u, depth+1);
}
}
void dfs(int u, int fa, int tot, int one) {
dp[u] = 0;
dp2[u] = 0;
map<double,pair<int,bool>> maps; // 0不保留 1保留
for (auto pi: g[u]) {
int v = pi.first;
if (v == fa) continue;
dfs(v, u, tot+1, one+pi.second);
double div = 1.0*(dp[v]+one+pi.second)/(tot+dp2[v]+1);
double div2 = 1.0*(one+pi.second)/(tot+1);
if (div<div2) maps[div] = {v, 1};
else maps[div2] = {v, 0};
}
if (maps.empty()) return;
if (dep[u]&1) { // O
auto pi = maps.begin()->second;
if (pi.second) { // reserve
dp[u] = dp[pi.first]+g[u][pi.first]; // one?
dp2[u] = dp2[pi.first]+1;
}
else {
dp[u] = g[u][pi.first]; // one?
dp2[u] = 1;
}
}
else { // G
auto pi = maps.rbegin()->second;
if (pi.second) { // reserve
dp[u] = dp[pi.first]+g[u][pi.first]; // one?
dp2[u] = dp2[pi.first]+1;
}
else {
dp[u] = g[u][pi.first]; // one?
dp2[u] = 1;
}
}
}
void solve() {
int n;
cin >> n;
fori(1, n) g[i].clear();
fori(1, n-1) {
int u,v,w;
cin >> u >> v >> w;
g[u].insert({v, w});
g[v].insert({u, w});
}
dfs0(1, 0, 0);
dfs(1, 0, 0, 0);
double ans = 1.0*dp[1]/dp2[1];
cout << ans << endl;
}
signed main() {
IOS;
int t;
cin >> t;
cout << fixed << setprecision(11);
while (t--) {
solve();
}
return 0;
}
牛客多校6D Puzzle: Wagiri
解题思路
这道题也是一个比较经典的图论题了。考察了无向图的双连通分量缩点的技巧。这道题用vDCC和eDCC都可以解决,我这里赛时用了董晓算法的vDCC板子。
首先先不考虑切边,先删掉所有不在环上的轮边,因为保留它是不符合条件的,因为我们不能加边。接下来我们就会得到一个由若干个DCC组成的图,考虑将这些DCC缩点,就会得到一个新的只有孤立点的图。这个时候我们采用Kruskal的思想(当然不需要对边进行排序),将剩余的切边一条一条加入新的图中,形成一棵生成树,最后判断所有的节点是否连通即可。时间复杂度 O ( n ) O(n) O(n)。
AC代码
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)
#define debug(x) cout << #x << " = " << x << endl
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
const int N = 1e5+7;
vector<pii> lun, qie;
vector<int> e[N];
int dfn[N], low[N], tot; // dfs序
int stk[N], top; // 手写栈
bool cut[N]; // 判断某个点是否为割点
vector<int> dcc[N]; // 点双连通分量
int root, cnt, num, id[N];
void tarjan(int x) {
dfn[x] = low[x] = ++tot;
stk[++top] = x;
if (x == root&&!e[x].size()) { // 孤立点
dcc[++cnt].push_back(x);
return;
}
int child = 0;
for (int y: e[x]){
if(!dfn[y]) {//若y尚未访问
tarjan(y);
low[x]=min(low[x],low[y]);
if (low[y]>=dfn[x]) {
child++;
if(x!=root||child>1) cut[x]=true;
int z; cnt++;
do { //记录vDCC
z=stk[top--];
dcc[cnt].push_back(z);
} while(z!=y);
dcc[cnt].push_back(x);
}
}
else low[x] = min(low[x],dfn[y]);
}
}
vector<int> ne[N];
int uni[N];
void init(int n) {
fori(0, n) uni[i] = i;
}
int find(int x) {
return x == uni[x]?x:(uni[x] = find(uni[x]));
}
void solve() {
int n,m;
cin >> n >> m;
fori(1, m) {
int u,v;
cin >> u >> v;
string str;
cin >> str;
if (str[0] == 'L') {
lun.push_back({u, v});
e[u].push_back(v);
e[v].push_back(u);
}
else qie.push_back({u, v});
}
for (root=1;root<=n;root++) if(!dfn[root]) tarjan(root);
init(n);
vector<pii> ans1;
// merge
fori(1, cnt) if (dcc[i].size()>=3) for (auto v: dcc[i]) uni[find(v)] = uni[find(*dcc[i].begin())];
for (auto pi: lun) if (find(pi.first) == find(pi.second)) ans1.emplace_back(pi);
for (auto pi: qie) {
if (find(pi.first) == find(pi.second)) continue;
ne[find(pi.first)].emplace_back(find(pi.second));
ne[find(pi.second)].emplace_back(find(pi.first));
uni[find(pi.first)] = uni[find(pi.second)];
ans1.emplace_back(pi);
}
bool can = true;
fori(1, n) {
if (find(i) == find(1)) continue;
can = false;
break;
}
if (can) {
YES;
cout << ans1.size() << endl;
for (auto pi: ans1) {
cout << pi.first << " " << pi.second << endl;
}
}
else NO;
}
signed main() {
IOS;
int t = 1;
while (t--) {
solve();
}
return 0;
}
牛客多校6F Challenge NPC 2
解题思路
这题因为没有特判 n = 3 n=3 n=3的情况导致一直WA,红温了。
思路和官方题解基本一样,如果给的图是一个菊花,那么一定无解。否则一定有解。特判 n = 3 n=3 n=3的所有情况并输出结果,剩下的就是 n ≥ 4 n\ge 4 n≥4的情况了。这个时候对于一个森林,我们可以将其每一棵树的直径串起来,也就是在每一棵树直径两端各加一条边连接上一颗树的尾部和下一棵树的首部。这个时候我们可以发现直径长度一定是 ≥ 4 \ge 4 ≥4的。
然后,从这棵大树的直径一端开始分层bfs,分两层即可,将相邻节点放到不同的层当中,交替放入。最后需要注意的点是:需要先输出第二层,再输出第一层,因为这样可以有效防止 n = 4 n=4 n=4时两层之间出现点相邻情况。时间复杂度 O ( n ) O(n) O(n)。
AC代码
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)
#define debug(x) cout << #x << " = " << x << endl
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
const int N = 5e5+7;
vector<int> g[N];
int deg[N];
bool vis[N];
int uni[N];
void init(int n) {
fori(1, n) uni[i] = i;
}
int find(int x) {
return x == uni[x]?x:(uni[x] = find(uni[x]));
}
vector<int> ans1, ans2;
int depth = 0, ma = 0;
void dfs(int u, int fa, int dep) {
if (dep>depth) {
depth = dep;
ma = u;
}
for (auto v: g[u]) {
if (v == fa) continue;
dfs(v, u, dep+1);
}
}
void bfs(int u) {
queue<int> que1, que2;
que1.push(u);
while (que1.size()||que2.size()) {
while (que1.size()) {
auto v = que1.front();
que1.pop();
vis[v] = true;
ans1.push_back(v);
for (auto w: g[v]) {
if (vis[w]) continue;
que2.push(w);
}
}
while (que2.size()) {
auto v = que2.front();
que2.pop();
vis[v] = true;
ans2.push_back(v);
for (auto w: g[v]) {
if (vis[w]) continue;
que1.push(w);
}
}
}
}
set<int> gg[N];
void solve() {
int n,m;
cin >> n >> m;
init(n);
ans1.clear(); ans2.clear();
fori(1, n) {
deg[i] = 0;
g[i].clear();
gg[i].clear();
vis[i] = false;
}
fori(1, m) {
int u,v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
gg[u].insert(v);
gg[v].insert(u);
uni[find(u)] = uni[find(v)];
deg[u]++; deg[v]++;
}
fori(1, n) if (deg[i] >= n-1) {
cout << -1 << endl;
return;
}
if (n == 3) {
if (gg[1].find(2) != gg[1].end()) cout << "1 3 2" << endl;
else if (gg[1].find(3) != gg[1].end()) cout << "1 2 3" << endl;
else if (gg[2].find(3) != gg[2].end()) cout << "2 1 3" << endl;
else cout << "1 2 3" << endl;
return;
}
set<int> sets;
fori(1, n) sets.insert(find(i));
int en = -1;
for (auto s: sets) {
depth = 0, ma = s;
dfs(s, 0, 0);
int now = ma;
depth = 0;
dfs(now, 0, 0);
if (en != -1) {
g[now].push_back(en);
g[en].push_back(now);
}
en = ma;
}
depth = 0;
dfs(ma, 0, 0);
bfs(ma);
for (auto u: ans1) ans2.push_back(u);
cout << ans2[0] << " ";
fori(1, n-1) {
int u = ans2[i];
cout << u << " ";
}
cout << endl;
}
signed main() {
IOS;
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
牛客多校6I Intersecting Intervals
解题思路
赛后补题,看了看题解的状态转移方程,恍然大悟,觉得自己的dp学的依托。
dp方程非常好理解:设
d
p
i
,
j
dp_{i,j}
dpi,j为第
i
i
i行强制选择第
j
j
j个元素的前
i
i
i行的最大值。
p
r
e
i
,
j
pre_{i,j}
prei,j为第
i
i
i行以第
j
j
j列结尾的最大子数组和(可以为0),
s
u
f
i
,
j
suf_{i,j}
sufi,j为第
i
i
i行以第
j
j
j列开头的最大子数组和(可以为0),
s
u
m
i
,
j
sum_{i,j}
sumi,j为第
i
i
i行第
j
j
j列的前缀和。转移方程如下:
d
p
i
,
j
=
m
a
x
{
d
p
i
−
1
,
k
+
s
u
m
i
,
j
−
s
u
m
i
,
k
−
1
+
p
r
e
i
,
k
−
1
+
s
u
f
i
,
j
+
1
,
k
≤
j
d
p
i
−
1
,
k
+
s
u
m
i
,
k
−
s
u
m
i
,
j
−
1
+
p
r
e
i
,
j
−
1
+
s
u
f
i
,
k
+
1
,
k
>
j
dp_{i,j}=max \left\{ \begin{aligned} dp_{i-1,k}+sum_{i,j}-sum_{i,k-1}+pre_{i,k-1}+suf_{i,j+1}, & k\le j\\ dp_{i-1,k}+sum_{i,k}-sum_{i,j-1}+pre_{i,j-1}+suf_{i,k+1}, & k> j\\ \end{aligned} \right.
dpi,j=max{dpi−1,k+sumi,j−sumi,k−1+prei,k−1+sufi,j+1,dpi−1,k+sumi,k−sumi,j−1+prei,j−1+sufi,k+1,k≤jk>j
最后遍历一遍 d p n , k dp_{n,k} dpn,k输出最大值即可。
但要注意,转移方程时,如果直接像上面的方程一样暴力转移,时间复杂度是 O ( n 3 ) O(n^3) O(n3)的,考虑优化转移。
观察转移方程可以发现,当
j
≥
k
j \ge k
j≥k时,我们可以将转移方程写成如下形式:
d
p
i
,
j
−
s
u
f
i
,
j
+
1
−
s
u
m
i
,
j
=
d
p
i
−
1
,
k
−
s
u
m
i
,
k
−
1
+
p
r
e
i
,
k
−
1
dp_{i,j}-suf_{i,j+1}-sum_{i,j}=dp_{i-1,k}-sum_{i, k-1}+pre_{i, k-1}
dpi,j−sufi,j+1−sumi,j=dpi−1,k−sumi,k−1+prei,k−1
可以发现,式子的左侧只和
j
j
j的取值有关,而式子的右侧只和
k
k
k的取值有关。因此我们可以考虑在每一行转移前预处理出右侧式子的最大值(
j
>
k
j > k
j>k时同理),然后更新状态时直接遍历一遍
d
p
dp
dp数组,然后取对应端点的max即可。具体做法详见代码。
AC代码
#include <bits/stdc++.h>
#pragma comment(linker, "/STACK:1073741824,1073741824")
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)
#define debug(x) cout << #x << " = " << x << endl
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
void solve() {
int n,m;
cin >> n >> m;
vector<vector<ll>> arr(n+2, vector<ll>(m+2, 0));
fori(1, n) forj(1, m) cin >> arr[i][j];
vector<vector<ll>> pre(n+2, vector<ll>(m+2, 0));
vector<vector<ll>> suf(n+2, vector<ll>(m+2, 0));
vector<vector<ll>> sum(n+2, vector<ll>(m+2, 0));
vector<vector<ll>> pre1(n+2, vector<ll>(m+2, 0));
vector<vector<ll>> suf1(n+2, vector<ll>(m+2, 0));
vector<vector<ll>> dp(n+2, vector<ll>(m+2, 0));
fori(1, n) {
forj(1, m) {
pre[i][j] = max({pre[i][j-1]+arr[i][j], arr[i][j], 0ll});
sum[i][j] = sum[i][j-1]+arr[i][j];
}
for (int j=m;j>=1;--j) suf[i][j] = max({suf[i][j+1]+arr[i][j], arr[i][j], 0ll});
}
fori(1, n) {
pre1[i][1] = dp[i-1][1], suf1[i][m] = dp[i-1][m]+sum[i][m];
forj(2, m) pre1[i][j] = max(pre1[i][j-1], dp[i-1][j]+pre[i][j-1]-sum[i][j-1]);
for (int j=m-1;j>=1;--j) suf1[i][j] = max(suf1[i][j+1], dp[i-1][j]+sum[i][j]+suf[i][j+1]);
forj(1, m) dp[i][j] = max(pre1[i][j]+sum[i][j]+suf[i][j+1], suf1[i][j]-sum[i][j-1]+pre[i][j-1]);
}
ll ans = -1e18;
forj(1, m) ans = max(ans, dp[n][j]);
cout << ans << endl;
}
signed main() {
IOS;
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}