NOIP A层联测8
感觉以后模拟赛都应该认真改题。
\(100+100+0+100\),开题顺序 \(1-4-2-3\),以为打到12:00结果只打到11:30导致 T3 特殊性质和暴力都没写,后来也懒得写了。
T4 前一天刚做过究极弱化版,跟偷了题一样,切得挺顺利;T2 由于忘了 \(k\) 相等耽误了好久,幸好做出来了。
T1 集合(collection)
考虑计算每种子集和的出现次数,裸的背包,最后直接算,要用费马小定理。
#include<stdio.h>
#include<iostream>
#include<algorithm>
using namespace std;
const int MOD=998244353;
int n,f[25000];long long ans=1;
inline int ksm(long long a,int b)
{
long long ans=1;
while(b)
{
if(b&1) ans=ans*a%MOD;
a=a*a%MOD,b>>=1;
}
return ans;
}
int main()
{
#ifdef ONLINE_JUDGE
freopen("collection.in","r",stdin);
freopen("collection.out","w",stdout);
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
#endif
cin>>n;f[0]=1;
for(register int i=1;i<=n;++i)
for(register int j=(i+1)*i/2;j>=i;--j)
f[j]=(f[j]+f[j-i])%(MOD-1);
for(register int j=1;j<=(n+1)*n/2;++j)
ans=ans*ksm(j,f[j])%MOD;
cout<<ans<<'\n';return 0;
}
T2 出租(lantern)
考虑每次从前往后处理,这样显然尽可能往前放是对的,放不下的直接留给下一个位置考虑。
设 \(sum_i\) 为所有喜好位置为 \(i\) 的总人数。假设所有喜好位置小于 \(x\) 的都可以放下。放完 \(x\) 位置后,还没有规定位置的个数是以 \(x\) 为结尾的,\(sum_i-k\) 的最大后缀和。这些数要放到 \([x+1,x+d]\) 中,也就是说这个值是否小于等于 \(dk\) 等价于是否可行。
所以可以符合所有条件就等价于对于 \(\forall i\in[1,n-d]\),最大后缀和都小于等于 \(dk\)。
对于所有位置的最大后缀和就相当于全局的最大子段和。单点修改,全局最大子段和,线段树直接做。
#include<stdio.h>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN=5e5+10;
int n,m,d;long long k;
struct tree{long long num,sum,suml,sumr;}t[MAXN<<2];
inline void push_up(int p)
{
t[p].num=t[p<<1].num+t[p<<1|1].num;
t[p].sum=max(max(t[p<<1].sum,t[p<<1|1].sum),t[p<<1].sumr+t[p<<1|1].suml);
t[p].suml=max(t[p<<1].suml,t[p<<1].num+t[p<<1|1].suml);
t[p].sumr=max(t[p<<1|1].sumr,t[p<<1|1].num+t[p<<1].sumr);
return ;
}
void build(int l,int r,int p)
{
if(l==r){t[p].num=-k;return ;}
int mid=(l+r)>>1;
build(l,mid,p<<1),build(mid+1,r,p<<1|1);
push_up(p);return ;
}
void change(int l,int r,int p,int x,int z)
{
if(l==r)
{
t[p].num+=z;
t[p].sum=t[p].suml=t[p].sumr=max((long long)0,t[p].num);
return ;
}
int mid=(l+r)>>1;
if(x<=mid) change(l,mid,p<<1,x,z);
else change(mid+1,r,p<<1|1,x,z);
push_up(p);return ;
}
signed main()
{
#ifdef ONLINE_JUDGE
freopen("lantern.in","r",stdin);
freopen("lantern.out","w",stdout);
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
#endif
cin>>n>>m>>k>>d;build(1,n,1);
while(m--)
{
int x,y;cin>>x>>y;
change(1,n,1,x,y);
cout<<((t[1].sum<=k*d)?"YES":"NO")<<'\n';
}
return 0;
}
T3 连通块(connection)
后来发现是挺平凡的树形 DP,状态设计有点没想到。
定义 \(f_{i,j}\) 是以 \(i\) 为根的,dfs 序最后一位是第 \(j\) 号结点的最大连通块,最终答案为 \(\max\limits_{i,j\in[1,n]} f_{i,j}\),初始化 \(f_{i,i}=a_i\),其余为极小值。
考虑 \(y\) 是 \(x\) 的子节点,将 \(y\) 合并到 \(x\) 上。即 \(f_{i,j}=\max\limits_{j,k\in [1,n],k \text{ 和 } y \text{ 没有约束}} (f_{x,k}+f_{y,j})\),时间复杂度 \(O(n^3)\)。
发现其实对于同一个 \(y\) 每个 \(j\) 都是从同一个 \(k\) 转移过来的,所以先找出最大的 \(k\) 和 \(y\) 没有约束的 \(f_{x,k}\),再直接用这个值去 DP,时间复杂度 \(O(n^2)\)。
发现与约束有关的数 \(\leq 2m\) 个,所以可以重新编号,将所有与约束无关的点都看成等价的点,时间复杂度 \(O(nm)\)。
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<vector>
#include<string.h>
#define int long long
using namespace std;
const int MAXN=1e5+10,INF=1e16+7;
int n,m,a[MAXN],s[MAXN],h[MAXN],tot;
int ANS,f[MAXN][45];bool b[45][45];
vector <int> v[MAXN];
inline void dp(int x,int fa=0)
{
f[x][h[x]]=a[x];
for(int y:v[x])
{
if(y==fa) continue;dp(y,x);int MAX=-INF;
for(register int i=0;i<=tot;++i)
if(!b[i][h[y]]) MAX=max(MAX,f[x][i]);
for(register int j=0;j<=tot;++j)
f[x][j]=max(f[x][j],MAX+f[y][j]);
}
for(register int j=0;j<=tot;++j) ANS=max(ANS,f[x][j]);
return ;
}
signed main()
{
#ifdef ONLINE_JUDGE
freopen("connection.in","r",stdin);
freopen("connection.out","w",stdout);
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
#endif
cin>>n>>m;
for(register int i=1;i<=n;++i) cin>>a[i];
for(register int i=1;i<=n;++i)
{
cin>>s[i];
for(register int j=1,x;j<=s[i];++j)
cin>>x,v[i].push_back(x);
}
for(register int i=1,x,y;i<=m;++i)
{
cin>>x>>y;
if(!h[x]) h[x]=++tot;
if(!h[y]) h[y]=++tot;
b[h[x]][h[y]]=b[h[y]][h[x]]=1;
}
memset(f,-0x3f3f3f,sizeof(f));dp(1);
cout<<ANS<<'\n';return 0;
}
T4 跳棋(checkers)
考虑没有 ? 的情况。
将每个连续段中的 1 两两分组,总组数是 \(a\),所有剩余的单独出来的 1 的个数为 \(b\),结论是状态数是 \(\dbinom{n-a-b}{a}\),懒得写一遍,参考 AquaMoon and Chess。
对于有 ? 的情况。发现仅与 \(a\) 和 \(b\) 有关,而 \(n\leq 500\),考虑枚举 \(a,b\)。但是不知道每种 \(a,b\) 的出现次数,挺 DP 的,设 \(f_{i,x,y,0/1/2}\) 为填到 \(i\),有 \(x\) 个 \(a\),\(y\) 个 \(b\),\(i\) 位是 \(0\) \(/\) 作为一个单独的 1 的结尾 \(/\) 作为一个 11 组的结尾,的方案数。(PS:好像最后一维两种状态就行)
考虑第 \(i\) 位填 \(0\),\(f_{i,x,y,0}=\sum\limits_{k\in[0,2]} f_{i-1,x,y,k}\) 是显然的。
第 \(i\) 位填 \(1\) 的话,如果第 \(i-1\) 位状态是 \(0\) 或 \(2\),情况都是添加了一个单独的 1,所以 \(f_{i,x,y,1}=f_{i-1,x,y-1,0}+f_{i-1,x,y-1,2}\)。
如果 \(i-1\) 位状态是 \(1\),那就相当于你减少了一个单独的 1 而增加了一组 11,所以要从 \(x-1,y+1\) 转移过来,即 \(f_{i,x,y,2}=f_{i-1,x-1,y+1,1}\)。
最后直接枚举 \(a,b\) 算对应方案数即可。DP 时滚一下数组。
#include<stdio.h>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN=510,MOD=1e9+7;
int n,f[2][255][255][3],s[255][255];
char c[MAXN];long long ans,P[MAXN],inv[MAXN];
inline long long C(int n,int m)
{
if(n<m) return 0;
return P[n]*inv[m]%MOD*inv[n-m]%MOD;
}
inline long long ksm(long long a,int b)
{
long long ans=1;
while(b)
{
if(b&1) ans=ans*a%MOD;
a=a*a%MOD,b>>=1;
}
return ans;
}
int main()
{
#ifdef ONLINE_JUDGE
freopen("checkers.in","r",stdin);
freopen("checkers.out","w",stdout);
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
#endif
cin>>n;P[0]=1;f[0][0][0][0]=1;
for(register int i=1;i<=n;++i) cin>>c[i],P[i]=P[i-1]*i%MOD;
inv[n]=ksm(P[n],MOD-2);
for(register int i=n-1;i>=0;--i) inv[i]=inv[i+1]*(i+1)%MOD;
for(register int i=1;i<=n;++i)
for(register int x=0;x<=n/2;++x)
for(register int y=0;y<=min((n+1)/2,n-2*x);++y)
{
for(register int k=0;k<=2;++k) f[i&1][x][y][k]=0;
if(c[i]!='1')
for(register int k=0;k<=2;++k)
f[i&1][x][y][0]=(f[i&1][x][y][0]+f[i&1^1][x][y][k])%MOD;
if(c[i]!='0')
{
if(y>=1)
f[i&1][x][y][1]=(f[i&1^1][x][y-1][0]+f[i&1^1][x][y-1][2])%MOD;
if(x>=1)
f[i&1][x][y][2]=f[i&1^1][x-1][y+1][1];
}
}
for(register int x=0;x<=n/2;++x)
for(register int y=0;y<=min((n+1)/2,n-2*x);++y)
{
int sum=0;
for(register int k=0;k<=2;++k) sum=(sum+f[n&1][x][y][k])%MOD;
ans+=C(n-x-y,x)*sum%MOD;
}
cout<<ans%MOD<<'\n';return 0;
}

浙公网安备 33010602011771号