day 11 模拟赛 6
模拟赛 6
T1 卷
给定 n 个非负整数数组 a,b ,求
\[\sum_{i=1}^n\sum_{j=1}^n⌊\sqrt{|a_i-b_j|}⌋ \]\(n\le 10^6\),\(\sum a_i,\sum b_i\le 10^7\)。
算法一:暴力枚举 \(a_i,b_j\),复杂度 \(O(n^2)\)。-------------------------------30分
算法二:对于 \(a_i,b_i\le 10^3\) 的数据,统计每个 \(a_i,b_j\) 的出现次数,对于每组 \(a_i,b_j\) 累加贡献:\(cnt\times ⌊\sqrt{|a_i-b_j|}⌋\)。-------------------------------50分
算法三:注意到 \(\sum a_i,\sum b_i\le 10^7\),发现不同的 \(a_i,b_i\) 很少,当 \(a_i\) 为最小的 n 个整数时,\(\sum a_i=(n-1)n/2\),所以不同的 \(a_i,b_i\) 是 \(O(\sqrt{\sum a_i+\sum b_i})\) 级别的,故我们对 a,b 进行离散化,暴力枚举 a,b,每次像算法二那样计算贡献,复杂度 \(O(\sum a_i+\sum b_i)\)。-------------------------------100分
代码比较懒,用 map 多了一个 log。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,maxn;
map<int,int>a;
map<int,int>b;
signed main(){
freopen("fft.in","r",stdin);
freopen("fft.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie();
cin>>n;
for(int i=1;i<=n;i++){
int x;cin>>x;
a[x]++;
maxn=max(maxn,x);
}
for(int i=1;i<=n;i++){
int x;cin>>x;
b[x]++;
maxn=max(maxn,x);
}
int ans=0;
for(auto [i,cnti]:a){
for(auto [j,cntj]:b){
ans+=cnti*cntj*(int)sqrt(abs(i-j));
}
}
cout<<ans;
return 0;
}
T2 王牌对王牌
小洛和机器人玩卡牌游戏,牌堆共有
n张牌(n为偶数),牌堆顶是“先摸到的牌”,三种卡牌:
- 学习(1):直接得 1 分。
- 偷卷(2):立即从牌堆额外摸 2 张牌(如果牌堆还有牌)。
- 反转(3):取消上一张“偷卷”的效果;如果对“反转”用,则恢复再前面一张牌的效果。
规则:
- 双方初始无手牌,每回合开始时各自摸 2 张牌,轮流进行。
- AI 机器人只会出“学习”和“反转”:自己回合尽可能多出“学习”;对方出牌后,如果手中有“反转”会立即打出。
- 小洛只关心自己能获得的最大分数,不关心 AI 的得分。
求小洛能获得的最大分数。
\(n\le 300\),n 为偶数。
算法一:(与正解无关)观察题意,需要决策的只有何时出偷卷,对于学习,获得之后什么时候打出的效果都是一样的,对于反转,在对方反转我们的偷卷是打出即可。
所以我们暴力 DFS 每次是否使用偷卷,然后进行模拟,取最大得分即可,可以获得 \(n\le 10\) 的部分分。-------------------------------20分
算法二:本题显然可以尝试 DP,先考虑特殊性质 \(a_i\le 2\),即没有反转,记 \(dp[i][j][0/1]\) 表示当前牌堆的前 \(2i\) 张牌已经被拿走,玩家有 j 张偷卷时,0/1 表示轮到玩家还是 AI 行动,此时可以获得的最大得分。
显然有转移
其中 a,b 表示牌堆上方两张牌的学习、偷卷数量。
两个式子分别是摸牌、用偷卷牌的两个转移。(注意第二行的偷卷操作是在摸牌之后操作的,所以从状态 1 转移)
注意为什么只转移一次?因为第一维的状态是按出牌数量设定的,如果连续出偷卷,那么剩下的转移,将交给之后转移。-------------------------------40分
算法三:(与正解无关)加入反转后怎么做?先考虑一种最暴力的 DP,把什么都记下来:
记 \(dp[i][j][k_1][k_2][0/1]\),其中 \(k_1,k_2\) 分别为玩家和 AI 此时有多少反转。
因为有反转,所以玩家出偷卷牌不被抵消的条件为 \(k_1\ge k_2\),消耗完偷卷牌后双方分别剩余 \(k_1-k_2,0\) 张偷卷,时间和空间复杂度为 \(O(n^4)\)。-------------------------------80分
算法四:注意到我们现在记录的状态有些多余,考虑如何优化掉一维。
记 \(dp[i][j][k][0/1]\) 为表示当前牌堆的前 \(2i\) 张牌已经被拿走,玩家有 j 张偷卷,玩家偷卷+反转数量,加上 AI 反转数量为 k 时,0/1 表示轮到玩家还是 AI 行动,此时可以获得的最大得分。
为什么这样设置 k?因为玩家如果想摸牌,就要拼命打偷卷和反转,以应对对方的反转,所以如果 玩家偷卷+玩家反转-对方反转>1,那么在消耗之后,玩家就还有至少 1 张偷卷牌,也就是 \(k>1\) 时可以转移。这样就把判定能否偷卷的条件从两维优化到一维。
考虑消耗之后 j,k 的变化,不算摸牌,\(j=\min(j,k)-1,k=k-1\)。
最终转移式如下:
其中 a,b,c 表示牌堆上方两张牌的学习、偷卷、反转数量。
三个式子分别表示玩家摸牌、使用偷卷、AI 摸牌。
实现细节:
- k 有可能是负数,所以要设置一个偏移,使下标不为负数。
- 目前的 dp 数组空间开不下,第一维要设置为滚动数组。
时间复杂度 \(O(n^3)\),空间复杂度 \(O(n^2)\)。-------------------------------100分
#include<bits/stdc++.h>
using namespace std;
int t,n,a[350];
int dp[2][350][650][2];
int main(){
freopen("duel.in","r",stdin);
freopen("duel.out","w",stdout);
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
int mid=310;
for(int i=0;i<=1;i++){
for(int j=0;j<=n;j++){
for(int k=mid-n-1;k<=mid+n+1;k++){
dp[i][j][k][0]=dp[i][j][k][1]=-1e9;
}
}
}
dp[0][0][mid][0]=0;
for(int i=0;i<=n/2;i++){
for(int j=0;j<=n;j++){
for(int k=mid-n-1;k<=mid+n+1;k++){
dp[(i%2)^1][j][k][0]=dp[(i%2)^1][j][k][1]=-1e9;
}
}
for(int j=0;j<=n;j++){
for(int k=mid-n-1;k<=mid+n+1;k++){
int mo[4]={0,0,0,0};
mo[a[i*2+1]]++;mo[a[i*2+2]]++;
dp[(i%2)^1][j+mo[2]][k+mo[2]+mo[3]][1]=max(dp[(i%2)^1][j+mo[2]][k+mo[2]+mo[3]][1],dp[i%2][j][k][0]+mo[1]);
if(j>=1&&k-mid>=1)dp[(i%2)^1][min(j,k-mid)-1+mo[2]][k-1+mo[2]+mo[3]][1]=max(dp[(i%2)^1][min(j,k-mid)-1+mo[2]][k-1+mo[2]+mo[3]][1],dp[i%2][j][k][1]+mo[1]);
dp[(i%2)^1][j][k-mo[3]][0]=max(dp[(i%2)^1][j][k-mo[3]][0],dp[i%2][j][k][1]);
}
}
}
int ans=0;
for(int j=0;j<=n;j++){
for(int k=mid-n-1;k<=mid+n+1;k++){
ans=max(ans,max(dp[n/2%2][j][k][0],dp[n/2%2][j][k][1]));
}
}
cout<<ans<<"\n";
}
return 0;
}
T3 博物馆
给定一棵 n 个点的树。你需要在树上放置若干条路径(两个端点之间的最短路径),要求:
覆盖所有点和所有边:每个点和每条边都至少被某条路径“看到”(路径经过的点/边即被看到)。
路径两两不相交:任意两条路径不能有公共点。
最小化路径数量:在满足上述条件的前提下,用的路径越少越好。
计数:在路径数量最少的方案中,有多少种不同的方案(方案不同当且仅当存在一条路径在一种方案中出现而在另一种中没有)。
\(n\le 5\times 10^5\)
树形 DP,记 \(dp[u][0/1][0/1/2]\) 为以结点 u 为根的子树,结点 u 在/不在 链上,结点 u 在链上的度为 0/1/2(为 0 时说明这个链只有 u 一个结点,为 1 时说明 u 是这个链的端点,为 2 时说明 u 是这个链中间的一点)。实际上,第二维为 0 时,第三维必定为 0。
考虑如何转移,设 u 为 v 的父亲,通过限制每个点到链的长度不超过 1 可得,如果 u 不在链上,那么它的所有子树 v 必定被链覆盖,否则连接 u,v 的边将不会被看到,所以 u,v 至少有一个在链上,转移时也就至少有一个第二维是 1。
所以,我们枚举 u,v 的两个状态(在不在链上/链上度数),如果至少有一个在链上,就可以把子树 v 中的最小值加到 u 中,和之前的值取 min。
但是有一个特殊的情况,就是如果 u,v 都是链的端点(第三维为 0/1),就可以连接两个链,把两边的链合成一个经过结点 u,v 的链,转移之后点 u 的度数会 +1,链的数量因为合并,会减少 1。
最终答案即为 \(dp[1]\) 中的最小值。
考虑如何统计方案,在 dp 转移的过程中同步处理即可,做的是乘法,注意让链的数量最小是优先的,不能直接对方案数取 min/max,要依着方案数最小,代码中写了一个 upd 函数用来更新。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int INF=1e9,N=500005,MOD=998244353;
int t,n;
vector<int>g[N];
struct node{
int first,second;//分别存储最小链数量和方案数
}dp[N][2][3];
//用来更新 dp 的两个值
void upd(node &x,int y,int z){
if(y<x.first)x={y,z};
else if(y==x.first)x={y,(x.second+z)%MOD};
}
void dfs(int u,int fa){
memset(dp[u],0x3f,sizeof dp[u]);
dp[u][0][0]={0,1};
dp[u][1][0]={1,1};
for(int v:g[u]){
if(v==fa)continue;
dfs(v,u);
node f[2][3];//转移需借用临时数组
memset(f,0x3f,sizeof f);
for (int a=0;a<=1;a++){
for (int i=0;i<=2;i++){
if(dp[u][a][i].first>=INF){
continue;
}
for (int b=0;b<=1;b++){
//至少有一个需要被链覆盖
if(!a&&!b){
continue;
}
for (int j=0;j<=2;j++){
if(dp[v][b][j].first>=INF){
continue;
}
//合并最小链数和方案数
int vv=(dp[u][a][i].first+dp[v][b][j].first)%MOD;
int w=(1ll*dp[u][a][i].second*dp[v][b][j].second)%MOD;
upd(f[a][i],vv,w);
//都是端点,可以合并链
if(a&&b&&i<2&&j<2){
upd(f[a][i+1],vv-1,w);
}
}
}
}
}
memcpy(dp[u],f,sizeof(f));//这个函数可以把一段内存复制到另一段中
}
}
signed main(){
freopen("museum.in","r",stdin);
freopen("museum.out","w",stdout);
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++)g[i].clear();
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,-1);
node ans=dp[1][0][0];
upd(ans,dp[1][1][0].first,dp[1][1][0].second);
upd(ans,dp[1][1][1].first,dp[1][1][1].second);
upd(ans,dp[1][1][2].first,dp[1][1][2].second);
cout<<ans.first<<" "<<ans.second<<"\n";
}
return 0;
}
浙公网安备 33010602011771号