20250609测试总结
T1 P4063 [JXOI2017] 数列
Description
九条可怜手上有一个长度为 \(n\) 的整数数列 \(r_i\),她现在想要构造一个长度为 \(n\) 的,满足如下条件的整数数列 \(A\) :
-
\(1 \le A_i \le r_i\)
-
对于任意 \(3 \le i \le n\) ,令 \(R\) 为 \(A_1\) 至 \(A_{i-2}\) 中大于等于 \(A_{i-1}\) 的最小值,\(L\) 为 \(A_1\) 至 \(A_{i-2}\) 中小于等于 \(A_{i-1}\) 的最大值。\(A_i\) 必须满足 \(L \le A_i \le R\) 。如果不存在大于等于 \(A_{i-1}\) 的,那么 \(R=+\infty\) ;如果不存在小于等于 $A_{i-1} $ 的,那么 \(L = -\infty\) 。
现在可怜想要知道共有多少不同的数列满足这个条件。两个数列 \(A\) 和 \(B\) 是不同的当且仅当至少存在一个位置 \(i\) 满足 \(A_i \neq B_i\) 。
\(n\le50\),\(r_i\le150\)
Solution
赛时写了一个很显然的 dp 。
从前往后填,前边的有效限制信息为:上一个是什么,下界,上界。
因此设 \(dp[i][l][k][r]\) 表示填数范围 \(l\sim r\) 且上一个填的是 \(k\)。
转移:
-
填 \(l\):\(dp[i+1][l][l][l] \gets dp[i][l][k][r]\)。
-
填 \(l+1\sim k-1\):\(dp[i+1][l][k'][k] \gets dp[i][l][k][r]\)。
-
填 \(k\):\(dp[i+1][k][k][k] \gets dp[i][l][k][r]\)。
-
填 \(k+1\sim r-1\):\(dp[i+1][k][k'][r] \gets dp[i][l][k][r]\)。
-
填 \(r\):\(dp[i+1][r][r][r] \gets dp[i][l][k][r]\)。
注意第 \(1\) 条和第 \(5\) 条转移分别要求 \(l≠k\) 和 \(r≠k\)(否则就和第 \(3\) 条重复了)。
#include<bits/stdc++.h>
#define F(i,j,k) for(int i=j;i<=k;i++)
#define N 51
#define L 152
#define mod 998244353
using namespace std;
int dp[N][L][L][L],n,m[N],ans;
inline int mo(int x){return x<mod?x:x-mod;}
inline void add(int &x,int y){x=mo(x+y);}
inline void tran(int i,int l,int k,int r){
if(l!=k)add(dp[i+1][l][l][l],dp[i][l][k][r]);
F(j,l+1,k-1)add(dp[i+1][l][j][k],dp[i][l][k][r]);
add(dp[i+1][k][k][k],dp[i][l][k][r]);
F(j,k+1,r-1)add(dp[i+1][k][j][r],dp[i][l][k][r]);
if(k!=r)add(dp[i+1][r][r][r],dp[i][l][k][r]);
}
int main(){
cin>>n;
F(i,1,n)cin>>m[i];
F(i,1,m[1])dp[1][0][i][151]=1,tran(1,0,i,151);
F(i,2,n-1)F(k,1,m[i])F(l,0,k)F(r,k,151)if(dp[i][l][k][r])tran(i,l,k,r);
F(k,1,m[n])F(l,0,k)F(r,k,151)if(dp[n][l][k][r])add(ans,dp[n][l][k][r]);
cout<<ans;
}
时间复杂度 \(O(n\times r^4)\) 不过有剪枝跑不满。
(由于是 \(IOI\) 赛制,赛时过了,就没再想)
然而赛后打开题解发现正解时间复杂度 \(O(n\times r^3)\)。
选取比较有代表性的做法:
倒着填,可以发现当钦定后两个数的时候,再往前的位置只有一个区间不允许填的限制,于是把三位状态变成了两维。转移也比较正常,可能有几处细节。
T2 P5290 [十二省联考 2019] 春节十二响
Description
有一颗有根树,点有点权 \(a_i\) 。你需要把所有点分为若干类,要求每一类中的点不存在祖先关系。每一类的代价为其中的点的点权最大值。最终的代价为每一类代价之和。
要求最小化最终的代价并输出。
\(1\le n\le 2\times 10^5, 1\le a_i\le 10^9\)
Solution
先考虑整棵树为链(但是根节点不一定是链的一端)怎么做。
可以想到贪心:先制造链的较长的一端的“类”,对于另一端,新建类一定不优,那么向建好的类合并。想到贪心:分别排序,然后按照从大到小的顺序取 \(max\)。
于是对于普通情况一样操作,启发式合并即可。
时间复杂度 \(O(n\log n)\),复杂度证明类似于线段树合并。(注意不是 \(O(n\log^2 n)\))
#include<bits/stdc++.h>
#define ll long long
#define N 200005
#define pb push_back
using namespace std;
int n,m[N],f[N],dep[N],mxdp[N],son[N],res[N];
ll ans;
vector<int>t[N];
priority_queue<int>q[N];
inline void dfs(int u){
dep[u]=dep[f[u]]+1,mxdp[u]=dep[u];
for(int v:t[u]){
dfs(v),mxdp[u]=max(mxdp[u],mxdp[v]);
if(mxdp[v]>mxdp[son[u]])son[u]=v;
}
}
inline void dfs2(int u){
if(son[u])dfs2(son[u]),swap(q[son[u]],q[u]);
for(int v:t[u])if(v!=son[u]){
dfs2(v);
int tmp=q[v].size();
while(q[v].size()){
res[q[v].size()]=max(q[v].top(),q[u].top());
q[v].pop(),q[u].pop();
}
for(int i=1;i<=tmp;i++)q[u].push(res[i]);
}
q[u].push(m[u]);
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>m[i];
for(int i=2;i<=n;i++)cin>>f[i],t[f[i]].pb(i);
dfs(1),dfs2(1);
while(q[1].size())ans+=q[1].top(),q[1].pop();
cout<<ans;
}
T3 P5292 [HNOI2019] 校园旅行
Description
给定一个 \(n\) 个点 \(m\) 条边的图,点有 \(0/1\) 点权。\(q\) 次询问,给出 \(u,v\)$ 回答能否找到一条路径(不要求简单路径)使得经过的点的权值形成回文串。
\(1 \leq n \leq 5000\),\(1 \leq m \leq 5\times 10 ^ 5\),\(1 \leq q \leq 10 ^ 5\)
Solution
先考虑暴力,从合法点对开始记忆化搜索,每个点对最多进行一次操作,每次操作最坏情况下两个点分别枚举出边 O(m) 次,然而总边数为 \(m\) ,因此均摊时间复杂度 \(O(m^2)\) 。
#include<bits/stdc++.h>
#define pb push_back
#define N 5005
#define M 500005
using namespace std;
int n,m,q,tag[N];
bool a[N],ans[N][N];
struct edge{int u,v;}e[M];
char ch;
vector<int>t0[N],t1[N];
inline void dfs(int u,int v){
if(u>v)swap(u,v);
if(ans[u][v])return;
ans[u][v]=1;
for(int v1:t0[u])for(int v2:t0[v])dfs(v1,v2);
for(int v1:t1[u])for(int v2:t1[v])dfs(v1,v2);
}
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>q;
for(int i=1;i<=n;i++)cin>>ch,a[i]=ch=='1';
for(int i=1;i<=m;i++){
cin>>e[i].u>>e[i].v;
if(a[e[i].v])t1[e[i].u].pb(e[i].v);
else t0[e[i].u].pb(e[i].v);
if(a[e[i].u])t1[e[i].v].pb(e[i].u);
else t0[e[i].v].pb(e[i].u);
}
for(int i=1;i<=n;i++)dfs(i,i);
for(int i=1;i<=m;i++)if(a[e[i].u]==a[e[i].v])dfs(e[i].u,e[i].v);
for(int i=1,u,v;i<=q;i++){
cin>>u>>v;
cout<<(ans[min(u,v)][max(u,v)]?"YES\n":"NO\n");
}
}
考虑优化。注意到点数比较小,尝试去掉一些无用边。
对于一个回文串,左右对称的部位,可能是 01交替 或 只走0 或 只走1。我们单独考虑每一段。可以发现一段的奇偶性固定但长度不固定:要么只能为偶,要么只能为奇,要么随便。这取决于什么?取决于这种边是否构成二分图。如果不能构成,则随便。
进而想到,对于一个二分图,有用的是其任意一棵生成树。不是二分图呢?为了仍然能够满足随便到达,给它的任意一颗生成树加一条重边即可。
然后边数变为 \(O(n)\) 量级,跑上边的暴力即可。
#include<bits/stdc++.h>
#define pb push_back
#define N 5005
#define M 500005
using namespace std;
int n,m,q,fm[3][N];
bool a[N],vis[3][N],vis2[3][N],tag[3][N],ans[N][N];
char ch;
vector<int>t[3][N],g[N];
inline bool dfs(int k,int u,bool tg){
if(vis[k][u])return tag[k][u]^tg;
vis[k][u]=1,tag[k][u]=tg;
bool sf=0;
for(int v:t[k][u])sf|=dfs(k,v,tg^1);
return sf;
}
inline void dfs2(int k,int u){
vis[k][u]=0;
for(int v:t[k][u])if(vis[k][v])g[u].pb(v),g[v].pb(u),dfs2(k,v);
}
inline void dfs3(int u,int v){
if(u>v)swap(u,v);
if(ans[u][v])return;
ans[u][v]=1;
for(int v1:g[u])for(int v2:g[v])if(a[v1]==a[v2])dfs3(v1,v2);
for(int v1:g[u])for(int v2:g[v])if(a[v1]==a[v2])dfs3(v1,v2);
}
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>q;
for(int i=1;i<=n;i++)cin>>ch,a[i]=ch=='1';
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
if(a[u]&&a[v])t[0][u].pb(v),t[0][v].pb(u);
if(!a[u]&&!a[v])t[1][u].pb(v),t[1][v].pb(u);
if(a[u]^a[v])t[2][u].pb(v),t[2][v].pb(u);
}
for(int k=0;k<3;k++)for(int i=1;i<=n;i++)if(!vis[k][i])fm[k][i]=dfs(k,i,0)+1;
for(int k=0;k<3;k++)for(int i=1;i<=n;i++)if(fm[i])dfs2(k,i),fm[k][i]==2?g[i].pb(i),0:0;
for(int i=1;i<=n;i++)dfs3(i,i);
for(int u=1;u<=n;u++)for(int v:g[u])if(a[u]==a[v])dfs3(u,v);
for(int i=1,u,v;i<=q;i++)cin>>u>>v,cout<<(ans[min(u,v)][max(u,v)]?"YES\n":"NO\n");
}
总结:赛时 \(100+60+30\) 但是 \(T2\) 确实应该切掉。
其实,我一开始做链的时候随便写了一个用贪心合并数组(其实是对的),但是一个细节写错了,导致链的15pt就得了5pt,然后我就去看t3了。最后还剩10分钟的时候我发现问题了,改了一下,链的部分分过了然后就知道正常的树和链一样启发式合并就好,但是写不完了。

浙公网安备 33010602011771号