2023牛客国庆集训派对day7/牛客2020年暑期多校day7
Preface
又被打爆了,计数题做不来一点,被狠狠地拉开差距
而且这场前面写题跟吃了shi一样写什么什么挂,看个J题搞了快半小时才看懂题意,建议是速速\remake
随后喜提同题数罚时倒一,虽然最后20min突然想到了I题可以用prufer序列转化,但还是差了一点没写出来
由于今天出去打印东西了所以没太多时间补题,G题就先不管了以后再说(主要这题我比赛时候没咋想屁都不会)
A. Social Distancing
这题我们队成功从开场写到4h+的时候才过,从爆搜到各个version的DP都试了个遍,这里就直接讲最后写的复杂度较好的DP做法好了
首先仔细观察式子,我们发现原式等价于\(n\times \sum_{i=1}^n (x_i^2+y_i^2)-(\sum_{i=1}^n x_i)^2-(\sum_{i=1}^n y_i)^2\)
那么不妨考虑一个DP方程,设\(f_{i,X,Y}\)表示已经放了\(i\)个点,这些点的\(\sum x=X,\sum y=Y\)时,\(\sum x^2+y^2\)的最大值
这个DP的状态数是\(n\times (nr)^2\)的,转移也是\(r^2\),然后我本机跑了下跑的有点慢,何况还有多组数据
但后面发现题目要求的其实只有\(8\times 30=240\)种情况,那么可以直接本机打表输出即可
PS:由于徐神特判\(n=2\)的情况返回了\(r^2\)而不是\(4r^2\)所以下面代码会有针对这一种情况特判,主题算法就在solve(n,r)
中
打表代码
#include <bits/stdc++.h>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
//vector <array <int,3>> f[10];
const int S=300;
int f[10][605][605];
int solve(int n, int r) {
if(n == 1) return 0;
if(n == 2) return r * r;
RI i,j,k; vector <pi> pnt;
for (i=-r;i<=r;++i) for (j=-r;j<=r;++j)
if (i*i+j*j<=r*r) pnt.push_back(pi(i,j));
/*for (i=0;i<10;++i) f[i].clear();
f[0].push_back({0,0,0});
for (i=0;i<n;++i)
{
for (auto [x,y]:pnt) for (auto [pw,sx,sy]:f[i])
f[i+1].push_back({pw+x*x+y*y,sx+x,sy+y});
sort(f[i+1].begin(),f[i+1].end());
f[i+1].erase(unique(f[i+1].begin(),f[i+1].end()),f[i+1].end());
}
int ans=0; for (auto [pw,sx,sy]:f[n]) ans=max(ans,n*pw-sx-sy);
return ans;*/
for (i=0;i<=n;++i) for (j=-r*n;j<=r*n;++j)
for (k=-r*n;k<=r*n;++k) f[i][j+S][k+S]=0;
for (i=0;i<n;++i) for (j=-r*i;j<=r*i;++j)
for (k=-r*i;k<=r*i;++k) for (auto [x,y]:pnt)
f[i+1][j+x+S][k+y+S]=max(f[i+1][j+x+S][k+y+S],f[i][j+S][k+S]+x*x+y*y);
int ans=0; for (j=-r*n;j<=r*n;++j)
for (k=-r*n;k<=r*n;++k) ans=max(ans,n*f[n][j+S][k+S]-j*j-k*k);
return ans;
}
int main(int argc, char** argv) noexcept {
freopen("A.cpp", "w", stdout);
printf(
"#include <bits/stdc++.h>\n\n"
"int ans[9][31] = {\n"
);
for(int n = 0; n <= 8; ++n) {
printf(" { %d", n);
for(int r = 1; r <= 30; ++r) {
fprintf(stderr, "Calculating solve(n = %d, r = %d) = ", n, r);
int s = solve(n, r);
fprintf(stderr, "%d\n", s);
printf(", %6d", s);
}
printf(" },\n");
}
printf(
"};\n"
"\n"
"int main() {\n"
" std::ios::sync_with_stdio(false);\n"
" int T, a, b; std::cin >> T;\n"
" while(T--) {\n"
" std::cin >> a >> b;\n"
" std::cout << ans[a][b] << char(10);\n"
" }\n"
" return 0;\n"
"}\n"
);
return 0;
}
提交的代码
#include <bits/stdc++.h>
int ans[9][31] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 2, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900 },
{ 3, 8, 32, 76, 130, 224, 312, 416, 554, 722, 896, 1064, 1248, 1512, 1746, 2016, 2264, 2600, 2888, 3218, 3584, 3912, 4344, 4712, 5138, 5612, 6062, 6536, 6984, 7520, 8084 },
{ 4, 16, 64, 144, 256, 400, 576, 784, 1024, 1296, 1600, 1936, 2304, 2704, 3136, 3600, 4096, 4624, 5184, 5776, 6400, 7056, 7744, 8464, 9216, 10000, 10816, 11664, 12544, 13456, 14400 },
{ 5, 24, 96, 218, 384, 624, 880, 1188, 1572, 2014, 2496, 2984, 3520, 4224, 4870, 5616, 6336, 7224, 8056, 9008, 9984, 10942, 12080, 13144, 14326, 15624, 16896, 18184, 19488, 20968, 22480 },
{ 6, 36, 144, 324, 576, 900, 1296, 1764, 2304, 2916, 3600, 4356, 5184, 6084, 7056, 8100, 9216, 10404, 11664, 12996, 14400, 15876, 17424, 19044, 20736, 22500, 24336, 26244, 28224, 30276, 32400 },
{ 7, 48, 192, 432, 768, 1224, 1740, 2356, 3102, 3954, 4896, 5872, 6960, 8280, 9564, 11016, 12456, 14160, 15816, 17666, 19584, 21500, 23688, 25808, 28122, 30624, 33120, 35664, 38266, 41200, 44076 },
{ 8, 64, 256, 576, 1024, 1600, 2304, 3136, 4096, 5184, 6400, 7744, 9216, 10816, 12544, 14400, 16384, 18496, 20736, 23104, 25600, 28224, 30976, 33856, 36864, 40000, 43264, 46656, 50176, 53824, 57600 },
};
int main() {
std::ios::sync_with_stdio(false);
int T, a, b; std::cin >> T;
while(T--) {
std::cin >> a >> b;
std::cout << (a==2?4:1)*ans[a][b] << char(10);
}
return 0;
}
B. Mask Allocation
这题祁神手玩后发现其实就是个类似于gcd
的过程,然后上去写了一发还感觉被自测的数据卡了,我直接帮他一波提交发现过了就很乐
#include<bits/stdc++.h>
using namespace std;
int t;
vector<int> ans;
void gcd(int a, int b){
if (0==b) return;
for (int i=0; i<a-a%b; ++i) ans.push_back(b);
gcd(b, a%b);
}
int main(){
scanf("%d", &t);
while (t--){
ans.clear();
int a, b;
scanf("%d%d", &a, &b);
if (a<b) swap(a, b);
gcd(a, b);
printf("%d\n", ans.size());
for (int x : ans) printf("%d ", x); puts("");
}
return 0;
}
C. A National Pandemic
徐神刚开始搞了个听起来很神的根号分治+换根DP重构的做法,但我转念一想发现不对其实有更好写的大力树剖做法,结果爬上去写了一波WA了后发现是线段树写错了,只能说今天的状态属实有点抽象
首先考虑第一个操作怎么处理,不难发现一次操作后\(F(y)+=w-dist(x,y)=w-dep_x-dep_y+2\times dep_{\operatorname{LCA}(x,y)}\)
我们不妨用全局变量\(sum\)记录\(w-dep_x\)的值,用\(cnt\)记录操作\(1\)的次数,这样就可以很容易地处理式子的前面部分了
而后面的那个式子的做法也很经典,不妨每次修改时将\(x\)到根路径上所有点的权值加上\(1\),然后查询时求\(y\)到根路径上所有点的点权和,最后乘\(2\)即可
总复杂度\(O(n\log^2 n)\)
#include<cstdio>
#include<iostream>
#include<vector>
#define int long long
#define RI int
#define CI const int&
using namespace std;
const int N=50005;
int t,n,m,opt,x,y,sum,cnt,fix[N],dep[N]; vector <int> v[N];
class Segment_Tree
{
private:
int sum[N<<2],tag[N<<2],len[N<<2];
inline void apply(CI now,CI mv)
{
sum[now]+=len[now]*mv; tag[now]+=mv;
}
inline void pushup(CI now)
{
sum[now]=sum[now<<1]+sum[now<<1|1];
}
inline void pushdown(CI now)
{
if (tag[now]) apply(now<<1,tag[now]),apply(now<<1|1,tag[now]),tag[now]=0;
}
public:
#define TN CI now=1,CI l=1,CI r=n
#define LS now<<1,l,mid
#define RS now<<1|1,mid+1,r
inline void build(TN)
{
sum[now]=tag[now]=0; len[now]=r-l+1;
if (l==r) return; int mid=l+r>>1; build(LS); build(RS);
}
inline void modify(CI beg,CI end,CI mv,TN)
{
if (beg<=l&&r<=end) return apply(now,mv); int mid=l+r>>1; pushdown(now);
if (beg<=mid) modify(beg,end,mv,LS); if (end>mid) modify(beg,end,mv,RS); pushup(now);
}
inline int query(CI beg,CI end,TN)
{
if (beg<=l&&r<=end) return sum[now]; int mid=l+r>>1,ret=0; pushdown(now);
if (beg<=mid) ret+=query(beg,end,LS); if (end>mid) ret+=query(beg,end,RS); return ret;
}
#undef TN
#undef LS
#undef RS
}SEG;
namespace Tree_Divide
{
int idx,top[N],sz[N],dfn[N],anc[N],son[N];
inline void DFS1(CI now=1,CI fa=0)
{
sz[now]=1; son[now]=0; dep[now]=dep[fa]+1; anc[now]=fa;
for (auto to:v[now]) if (to!=fa)
{
DFS1(to,now); sz[now]+=sz[to];
if (sz[to]>sz[son[now]]) son[now]=to;
}
}
inline void DFS2(CI now=1,CI tf=1)
{
dfn[now]=++idx; top[now]=tf;
if (son[now]) DFS2(son[now],tf);
for (auto to:v[now]) if (to!=anc[now]&&to!=son[now]) DFS2(to,to);
}
inline void modify(int x,int y,CI z)
{
while (top[x]!=top[y])
{
if (dep[top[x]]<dep[top[y]]) swap(x,y);
SEG.modify(dfn[top[x]],dfn[x],z); x=anc[top[x]];
}
if (dep[x]<dep[y]) swap(x,y);
SEG.modify(dfn[y],dfn[x],z);
}
inline int query(int x,int y,int ret=0)
{
while (top[x]!=top[y])
{
if (dep[top[x]]<dep[top[y]]) swap(x,y);
ret+=SEG.query(dfn[top[x]],dfn[x]); x=anc[top[x]];
}
if (dep[x]<dep[y]) swap(x,y);
return ret+SEG.query(dfn[y],dfn[x]);
}
};
using namespace Tree_Divide;
inline int calc(CI x)
{
return sum-cnt*dep[x]+2LL*query(1,x);
}
signed main()
{
//freopen("C.in","r",stdin); freopen("C.out","w",stdout);
for (scanf("%lld",&t);t;--t)
{
RI i; for (scanf("%lld%lld",&n,&m),i=1;i<=n;++i) fix[i]=0,v[i].clear();
for (i=1;i<n;++i) scanf("%lld%lld",&x,&y),v[x].push_back(y),v[y].push_back(x);
for (sum=cnt=idx=0,DFS1(),DFS2(),SEG.build(),i=1;i<=m;++i)
{
scanf("%lld%lld",&opt,&x);
if (opt==1) scanf("%lld",&y),sum+=y-dep[x],++cnt,modify(1,x,1);
else if (opt==2)
{
int tmp=calc(x)+fix[x]; if (tmp>0) fix[x]-=tmp;
} else printf("%lld\n",calc(x)+fix[x]);
}
}
return 0;
}
D. Fake News
诈骗题,徐神开场写个Python然后T了,后面一打表发现只有\(n=1/24\)合法,其它均不合法
结论的具体证明可以看这里,只能说出这种题就很搞人心态
PS:这题关流同步的cout
输出会T,要换puts
才能过,就尼玛抽象
#include <bits/stdc++.h>
int main() {
//std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int T; std::cin >> T;
while(T--) {
int64_t n; std::cin >> n;
puts((n == 1 || n == 24 ?
"Fake news!" :
"Nobody knows it better than me!"));
}
return 0;
}
E. NeoMole Synthesis
怎么又是像化学题的东西,弃了弃了
F. Tokens on the Tree
这是啥东西,据说是动态淀粉质?
G. Topo Counting
祁神比赛的时候思路频出,但后面一看题解怎么不是组合的做法呢岂可修,先坑了以后再说
H. Dividing
傻逼闪总因为不特判\(n=1\)把自己心态写炸了红温了可真是个小丑
这题手玩以下很容易发现当\(k\)固定时,合法的\(n\)满足\(n\%k=0/1\),然后写出式子也很简单:
直接除法分块求解即可,注意最后\(k>n\)的部分要记得加上,同时要特判\(n=1\)的情况
#include<cstdio>
#include<iostream>
#define int long long
#define RI int
#define CI const int&
using namespace std;
const int mod=1e9+7;
int n,k,ans,lim;
signed main()
{
//freopen("H.in","r",stdin); freopen("H.out","w",stdout);
RI l,r; scanf("%lld%lld",&n,&k); ans=n%mod;
if (n==1) return printf("%lld",k%mod),0;
for (l=2,lim=min(n,k);l<=lim;l=r+1) r=min(lim,n/(n/l)),(ans+=(r-l+1)%mod*(n/l)%mod)%=mod;
for (--n,l=2,lim=min(n,k);l<=lim;l=r+1) r=min(lim,n/(n/l)),(ans+=(r-l+1)%mod*(n/l+1)%mod)%=mod;
if (k>n) (ans+=k-n)%=mod; return printf("%lld",ans),0;
}
I. Valuable Forests
唉摸鱼到最后才想起来树的计数有prufer序列这个玩意,结果最后时间不够了没写出来
考虑一个点\(x\)的度数转化到prufer序列上就是它的出现次数加一,那么我们可以这样统计\(g_n\)表示\(n\)个点的无根树的权值和:
其中对于\([1,n]\)中的每个数,我们枚举它在长为\(n-2\)的prufer序列中的出现次数\(j\),然后统计答案
接下来考虑怎么处理森林的情况,比赛的时候就是想用纯组合式子来推,后面发现答案总是差一点,看来还是得DP
不妨设\(f_n\)表示\(n\)个点构成的森林的个数,不妨设\(T_n\)表示\(n\)个点构成的无根树的数量,这个就是\(n^{n-2}\),则有:
转移就是枚举\(n\)号点所在的树的大小\(i+1\),然后推一下就很显然了
最后考虑设\(Ans(n)\)表示\(n\)个点构成的森林对应的权值和,则转移为:
这个式子的意义同上,加入\(n\)号点时枚举形成的树的大小\(i+1\),考虑它的贡献会计入\(f_{n-i-1}\)次,而已经确定的部分则会乘上后面定下的方案数
最后注意由于模数可能\(\le 5000\)因此组合数要用递推法来求解,用阶乘可能会存在出现\(0\)的情况,总复杂度\(O(n^2)\)
#include<cstdio>
#include<iostream>
#define RI int
#define CI const int&
using namespace std;
const int N=5005;
int t,n,mod,pw[N][N],C[N][N],fact[N],ifac[N],f[N],g[N],ans[N];
inline void inc(int& x,CI y)
{
if ((x+=y)>=mod) x-=mod;
}
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
inline void init(CI n)
{
RI i,j; for (C[0][0]=i=1;i<=n;++i)
for (C[i][0]=j=1;j<=i;++j) C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
for (i=1;i<=n;++i) for (pw[i][0]=j=1;j<=n;++j) pw[i][j]=1LL*pw[i][j-1]*i%mod;
}
int main()
{
//freopen("I.in","r",stdin); freopen("I.out","w",stdout);
RI i,j; scanf("%d%d",&t,&mod); init(5000);
for (i=2;i<=5000;++i) for (j=0;j<=i-2;++j)
inc(g[i],1LL*C[i-2][j]*pw[i-1][i-2-j]%mod*(j+1)%mod*(j+1)%mod*i%mod);
for (f[0]=f[1]=1,i=2;i<=5000;++i) for (j=0;j<=i-1;++j)
inc(f[i],1LL*C[i-1][j]*(j>1?pw[j+1][j-1]:1)%mod*f[i-j-1]%mod);
for (i=2;i<=5000;++i) for (j=0;j<=i-1;++j)
inc(ans[i],1LL*C[i-1][j]*(1LL*(j>1?pw[j+1][j-1]:1)*ans[i-j-1]%mod+1LL*g[j+1]*f[i-j-1]%mod)%mod);
while (t--) scanf("%d",&n),printf("%d\n",ans[n]);
return 0;
}
J. Pointer Analysis
纯模拟题,难点在于读懂题意,然后比赛的时候写完发现没有考虑操作间的顺序遂下机反思
后面仔细一想其实只要暴力执行\(26+26=52\)次就一定可以涵盖所有情况了,然后又赶紧跑上去改了下就过了
#include<cstdio>
#include<iostream>
#include<string>
#define RI int
#define CI const int&
using namespace std;
string s[205]; int n,g[26][26],f[26][26][26];
inline void calc(void)
{
RI i,j,k; for (k=1;k<=n;++k)
{
if (s[k][1]=='.') // 3
{
for (i=0;i<26;++i) if (g[s[k][0]-'A'][i])
for (j=0;j<26;++j) if (g[s[k][6]-'A'][j]) f[i][s[k][2]-'a'][j]=1;
} else if (s[k][5]=='.') // 4
{
for (i=0;i<26;++i) if (g[s[k][4]-'A'][i])
for (j=0;j<26;++j) if (f[i][s[k][6]-'a'][j]) g[s[k][0]-'A'][j]=1;
} else if (s[k][4]>='a'&&s[k][4]<='z') // 1
{
g[s[k][0]-'A'][s[k][4]-'a']=1;
} else //2
{
for (i=0;i<26;++i) if (g[s[k][4]-'A'][i]) g[s[k][0]-'A'][i]=1;
}
}
}
int main()
{
//freopen("J.in","r",stdin); freopen("J.out","w",stdout);
RI i,j; for (scanf("%d\n",&n),i=1;i<=n;++i) getline(cin,s[i]);
for (i=0;i<52;++i) calc();
for (i=0;i<26;++i)
{
putchar('A'+i); printf(": ");
for (j=0;j<26;++j) if (g[i][j]) putchar('a'+j);
putchar('\n');
}
return 0;
}
Postscript
这就是多校题的魅力吗,给他们展示一下中国人最喜欢的计数和数据结构,然而我都做不来一点