作为一名高二老年选手来补一下我省去年的省选题。
D1T1:寻宝游戏
按顺序给出\(n\)个\(m\)位的二进制数\(a_i\),再在最前方添一个\(0\),
给出\(q\)次询问,每次询问给出一个同样长为\(m\)的二进制数\(r_i\),
要求在之前给出的\(n+1\)个二进制数的每相邻两个数的空位添加按位与运算符或按位或运算符,
一共\(n\)个,并使得这个算式得到的值为\(r_i\),求方案数。
\(n,q\le 1000,m\le 5000\)
暴力\(30\%\)不提。
对每一位分开考虑。
根据 [NOI2014]起床困难综合症 的理论或者自己手推,我们可以得到如下结论:
考虑\(r_i\)的第\(j\)位和在其之前的\(n\)个\(bit\),可以知道如果\(|0\)或者\(\&1\)对结果没有影响;
如果这一位上是\(1\),那么情况应该是在最后一个\(|1\)后不存在\(\&0\);
如果这一位上是\(0\),那么情况应该是在最后一个\(\&0\)后不存在\(|1\),或者既没有\(\&0\)也没有\(|1\)。
你看这里的条件都和最后一次进行的操作相关,所以我们倒着确定每一次放入的符号:
我们来看一看\(a_n\)和\(r_i\)对应位置上的不同情况。
\(x?0=0,x?1=1\) : \(\&\ |\)均可。
\(x?0=1\):如果放入\(\&\)运算符则结果必定为\(0\),因此这个运算符只能为\(|\);
\(x?1=0\):如果放入\(|\)运算符则结果必定为\(1\),因此这个运算符只能为\(\&\)。
综上所述,我们可以得出:
如果\(a_n\)和\(r_i\)同时出现了\(0-1,1-0\)两种情况,显然无解;
否则,如果\(a_n\)和\(r_i\)有任何一位不同\((0-1/1-0)\),那么运算符是可以唯一确定的;
此时将需要考虑的行减少一些并继续考虑\(a_{n-1}\);
否则,可以知道此时\(a_n=r_i\);
此时要分\(r_i\)是否含有\(0/1\)进行讨论:
如果\(r_i\)全为\(0\),那么在\(a_n\)前添加\(\&\)后,\(a_{1-n-1}\)之前的运算符可以随机添加;
答案加上\(2^{n-1}\),然后递归考虑添加\(|\)的情况;
\(r_i\)全为\(1\)同理。
如果\(r_i\)即有\(1\)又有\(0\),只能分别进行递归。
但是,在这一次递归之后仅需考虑\(r_i\)剩下的全为\(1\)的部分 或 剩下的全为\(0\)的部分,
这意味着之后不会再次出现这种情况。
使用\(bitset\)优化运算,复杂度为\(O(\frac{nmq}{32})\),可以通过\(70\%\)的数据点。O2就过了
#include
#define FL "a"
using namespace std;
typedef long long ll;
typedef double dd;
const int N=1e3+10;
const int mod=1e9+7;
inline ll read(){
ll data=0,w=1;char ch=getchar();
while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
if(ch=='-')w=-1,ch=getchar();
while(ch<='9'&&ch>='0')data=data*10+ch-48,ch=getchar();
return data*w;
}
inline void file(){
freopen(FL".in","r",stdin);
freopen(FL".out","w",stdout);
}
inline void upd(int &a,int b){a+=b;if(a>=mod)a-=mod;}
inline void dec(int &a,int b){a-=b;if(a<0)a+=mod;}
inline int poww(int a,int b){
int res=1;
for(;b;b>>=1,a=1ll*a*a%mod)
if(b&1)res=1ll*a*res%mod;
return res;
}
int n,m,q,ans,pw[1005];char s[5005];
typedef bitset<5000> line;
line a[N],b[N],r,rv,e[N],f[N],u,tmp;
inline void solve(int x,line now){
if(!x){if(!(r&now).any())upd(ans,1);return;}
int c=(e[x]&now).any(),d=(f[x]&now).any();
if(!c&&!d){
if((r&now).any()&&(rv&now).any())
solve(x-1,now&a[x]),solve(x-1,now&b[x]);
else upd(ans,pw[x-1]),solve(x-1,now);
}
if(!c&&d)solve(x-1,now&b[x]);
if(c&&!d)solve(x-1,now&a[x]);
}
int main()
{
n=read();m=read();q=read();
register int i,j;
for(i=0;i
考虑再次进行转化。发现分开考虑每一位后得出的结论类似于比较两个数的大小关系,
于是我们将一种方案抽象为一个长为\(n\)的二进制数\(now\),
\(0\)表示\(|\),\(1\)表示\(\&\),\(0-0\)表示在\(0\)前插入\(|\)。
那么我们之前推出的结论可以转化为:
考虑\(r_i\)的第\(j\)位和在其之前的\(n\)个\(bit\),
\(0-0\)和\(1-1\)对结果没有意义;
如果这一位上是\(1\),那么在最后一个\(0-1\)之后不存在\(1-0\);
如果这一位上是\(0\),那么在最后一个\(1-0\)之后不存在\(0-1\),或者不存在\(1-0/0-1\)的情况;
可以发现如果将这\(n\)个\(bit\)转化为一个二进制数\(b_j\),
这就是一个严格小于\((now
那么对于每个询问扣出其边界\(x\le now
对转化后的\(b_j\)排个序就好了。
复杂度为\(O(nmlogn+qm)\),可以通过\(100\%\)的数据点。
#include
#define FL "a"
using namespace std;
typedef long long ll;
typedef double dd;
const int N=1e3+10;
const int M=5e3+10;
const int mod=1e9+7;
inline ll read(){
ll data=0,w=1;char ch=getchar();
while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
if(ch=='-')w=-1,ch=getchar();
while(ch<='9'&&ch>='0')data=data*10+ch-48,ch=getchar();
return data*w;
}
inline void file(){
freopen(FL".in","r",stdin);
freopen(FL".out","w",stdout);
}
int n,m,q;char s[M];
int pw[N],a[N][M],r[N][M];
int val[M],o[M],p[M];
inline void upd(int &a,int b){a+=b;if(a>=mod)a-=mod;}
inline void dec(int &a,int b){a-=b;if(a<0)a+=mod;}
inline bool cmp(int i,int j){
for(int k=n;k;k--)
if(a[k][i]!=a[k][j])return a[k][i]=y){puts("0");continue;}
dec(res=val[o[y]],val[o[x]]);
printf("%d\n",res);
}
return 0;
}
D1T2:转盘
一个转盘上有摆成一圈的\(n\)个物品(编号1~\(n\)),其中的\(i\)个物品会在\(t_i\)时刻出现。
在0时刻时,小G可以任选\(n\)个物品中的一个,我们将其编号为\(s_0\)。
并且如果\(i\)时刻选择了物品\(s_i\),那么\(i+1\)时刻可以继续选择当前物品或选择下一个物品(\(s_i\%n+1\))。
在每一时刻(包括\(0\)时刻),如果小G选择的物品已经出现了,那么小G将会标记它。
小H想知道,在物品选择的最优策略下,小G什么时候能标记所有物品?
\(n,m,t_i\le 10^5\),带修+强制在线。
首先我们知道最优方案一定可以只绕一圈。
理由是假设最优方案中最后标记的哪一个物品为\(x\),
那么第一次直接从\(x+1\)开始然后等到其出现显然不会更劣。
直接暴力枚举起点维护答案时间复杂度为\(O(n^2m)\)。
考虑稍作转化,记录一个长度为\(2n\)的数组\(a\),\(a_i=T_i-i,a_{i+n}=a_{i}\),
那么答案为\(min_{i=1}^n\{max_{j=1}^n\{a_{i+j-1}\}+i\}+n-1\)。
经典的滑动窗口问题,使用单调队列维护最大值,时间复杂度降为\(O(nm)\)。
现在考虑如何快速维护这\(n\)个长为\(n\)的窗口。
根据题目性质,\(a_{i+n}=t_i-(i+n)
那么我们可以维护\(min_{i=1}^n\{max_{j=i}^{2n}\{a_{i+j-1}\}+i\}+n-1\)
于是考虑在线段树上维护最大值\(mx[x]\)和\(ans[x]=min_{i=l}^{mid}\{max_{j=i}^r\{a_j\}+i\}\),根据各个节点的情况讨论一下进行修改即可。
最后一个节点表示的区间为\([1,2n]\),则\(ans[rt]\)即为所求。
#include
#define FL "a"
using namespace std;
typedef long long ll;
typedef double dd;
const int N=2e5+20;
const int inf=2147483647;
const int mod=998244353;
inline ll read(){
ll data=0,w=1;char ch=getchar();
while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
if(ch=='-')w=-1,ch=getchar();
while(ch<='9'&&ch>='0')data=data*10+ch-48,ch=getchar();
return data*w;
}
inline void file(){
freopen(FL".in","r",stdin);
freopen(FL".out","w",stdout);
}
int n,m,p,a[N];
int mx[N<<2],ans[N<<2];
#define ls (i<<1)
#define rs (i<<1|1)
#define mid ((l+r)>>1)
inline int getans(int i,int l,int r,int x){
if(l==r)return l+max(mx[i],x);
if(mx[rs]>=x)return min(ans[i],getans(rs,mid+1,r,x));
return min(getans(ls,l,mid,x),mid+1+x);
}
inline void update(int i,int l,int r){
mx[i]=max(mx[ls],mx[rs]);ans[i]=getans(ls,l,mid,mx[rs]);
}
void build(int i,int l,int r){
if(l==r){mx[i]=a[l];ans[i]=inf;return;}
build(ls,l,mid);build(rs,mid+1,r);update(i,l,r);
}
void insert(int i,int l,int r,int p,int x){
if(l==r){mx[i]=x;return;}
p<=mid?insert(ls,l,mid,p,x):insert(rs,mid+1,r,p,x);update(i,l,r);
}
int main()
{
n=read();m=read();p=read();
for(int i=1;i<=n;i++)a[i]=a[i+n]=read();
for(int i=1;i<=2*n;i++)a[i]-=i;
int res;build(1,1,2*n);printf("%d\n",res=ans[1]+n-1);
for(int i=1,x,y;i<=m;i++){
x=read();y=read();if(p)x^=res,y^=res;
a[x]=y-x;insert(1,1,2*n,x,y-x);
a[x+n]=y-x-n;insert(1,1,2*n,x+n,y-x-n);
printf("%d\n",res=ans[1]+n-1);
}
return 0;
}
同时感谢litble的题解教会了我做这道题。
D1T3:毒瘤
求\(n\)个点\(m\)条边的独立集方案数。
\(n\le 10^5,m\le n+10\)
令\(k=m-n+1\),表示这个图比树多出了\(k\)条边。
一个简单的想法是暴力枚举\(k\)条边所对应的\(2k\)个点的情况然后\(O(n)\ dp\),可以获得\(55\)分。
写的时候已经知道要用虚树了,所以我强行把这\(2k\)个点套了一棵虚树上去,
然后我的\(dp\)状态是\(f[i][S]\)表示当前节点子树内对应选择情况的方案数,\(S\)的大小是\(2^{2k}\);
预处理出虚树的每一条边的两个端点在不同选择情况下对应的方案数,预处理的总复杂度是\(O(n)\)。
树形\(dp\)的同时将子树内关键点的选择情况合并,根据树形背包的复杂度,
这个东西的复杂度好象是\(O(2^{2k})\)。
发现不能直接开这么大的数组,所以使用指针根据子树内关键点的大小动态分配内存,空间减小了一半;
然后就卡着时间卡着空间过了这题。(
下面放一放我这题的丑陋代码
#include
#define FL "a"
using namespace std;
typedef long long ll;
typedef double dd;
const int N=1e5+20;
const int mod=998244353;
inline ll read(){
ll data=0,w=1;char ch=getchar();
while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
if(ch=='-')w=-1,ch=getchar();
while(ch<='9'&&ch>='0')data=data*10+ch-48,ch=getchar();
return data*w;
}
inline void file(){
freopen(FL".in","r",stdin);
freopen(FL".out","w",stdout);
}
void print(int x,int d){if(d)print(x>>1,d-1),putchar(48+(x&1));}
inline void upd(int &a,int b){a+=b;if(a>=mod)a-=mod;}
inline void dec(int &a,int b){a-=b;if(a<0)a+=mod;}
inline int poww(int a,int b){
int res=1;
for(;b;b>>=1,a=1ll*a*a%mod)
if(b&1)res=1ll*a*res%mod;
return res;
}
int n,m,e,rt,ans;
struct edge{int u,v;}E[N];
int F[N];int find(int x){return F[x]?F[x]=find(F[x]):x;}
int head[N],nxt[N<<1],to[N<<1],cnt;
inline void add(int u,int v){to[++cnt]=v;nxt[cnt]=head[u];head[u]=cnt;}
int headv[N],nxtv[N<<1],tov[N<<1],sum[N<<1][2][2],cnte;
inline void addv(int u,int v){
tov[++cnte]=v;nxtv[cnte]=headv[u];headv[u]=cnte;
}
int fa[N],dep[N],sz[N],son[N],top[N],w[N],fw[N],cntw;
inline bool cmp_w(int i,int j){return w[i]dep[top[v]]?u=fa[top[u]]:v=fa[top[v]];
return dep[u]>lw[u],s=all;;s=(s-1)&all){
tmp[s]=0;if(!s)break;
}
for(int d=0;d<2;d++)
for(int s=sub[u];;s=(s-1)&sub[u]){
for(int t=sub[v];;t=(t-1)&sub[v]){
if(g[u][c][s>>lw[u]]&&g[v][d][t>>lw[v]]&&sum[i][c][d])
upd(tmp[(s|t)>>lw[u]],1ll*g[u][c][s>>lw[u]]*g[v][d][t>>lw[v]]%mod*sum[i][c][d]%mod);
if(!t)break;
}
if(!s)break;
}
for(int all=(sub[u]|sub[v])>>lw[u],s=all;;s=(s-1)&all){
g[u][c][s]=tmp[s];if(!s)break;
}
}
sub[u]|=sub[v];
}
if(id[u]>lw[u],s=all;;s=(s-1)&all){
tmp[s]=0;if(!s)break;
}
for(int s=sub[u];;s=(s-1)&sub[u]){
upd(tmp[(s|1<>lw[u]],g[u][1][s>>lw[u]]);
if(!s)break;
}
for(int all=(sub[u]|1<>lw[u],s=all;;s=(s-1)&all){
g[u][1][s]=tmp[s];if(!s)break;
}
sub[u]|=1<>1]+1;
sort(t,t+p);p=unique(t,t+p)-t;sort(t,t+p,cmp_w);
for(int i=0;i>lw[rt]]);
if(!s)break;
}
printf("%d\n",ans);
return 0;
}
上面的愚蠢做法实际上忽略了一个重要的优化:只需要枚举多出的边中每一对点的选择情况。
就算直接枚举\(0-0,0-1,1-0\)三种情况也比上面的方法要好。
实际只须枚举两种情况:某个节点不选,另一个节点随意/这个节点要选,对应的节点强制不选。
然后直接\(dp\)就能有\(75\)分,再加上虚树即可无压力\(O(n+k2^k)\ AC\)。
D2T1:游戏
感觉是个神仙题啊,不知道为什么你们都把它当sb题切
看到\(y\le x\)的部分分感觉可以线段树上二分暴力搞搞,于是觉得正解也可以这样做
于是就陷入了无穷无尽的调试中...
我们考虑比暴力更加优秀一些的方法:记忆化搜索。然后就过了此题
首先我们把没有上锁的房间连成一块。
到了一个新的房间时,我们要保证它是已经被搜索过了的。
一个很简单的想法是,对于一扇上锁的门,如果钥匙在它左边,那么我们肯定先求解位于门右边的房间的答案,如果从左边可以到达右边,那么直接加上右边搜索的结果即可。
那么我们对于一扇上锁的门,从钥匙所在方向的反方向的房间向钥匙所在的方向的房间连边,代表先求解反方向;
然后按照拓扑序依次进行求解即可。
#include
#define FL "game"
using namespace std;
typedef long long ll;
typedef double dd;
const int N=1e6+20;
const int inf=2147483647;
const int mod=998244353;
inline ll read(){
ll data=0,w=1;char ch=getchar();
while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
if(ch=='-')w=-1,ch=getchar();
while(ch<='9'&&ch>='0')data=data*10+ch-48,ch=getchar();
return data*w;
}
inline void file(){
freopen(FL".in","r",stdin);
freopen(FL".out","w",stdout);
}
int n,m,k,q,key[N],id[N];
int d[N],head[N],nxt[N<<1],to[N<<1],cnt;
inline void add(int u,int v){
d[v]++;to[++cnt]=v;nxt[cnt]=head[u];head[u]=cnt;
}
queueQ;int L[N],R[N];
inline void solve(int i){
bool sl=0,sr=0;
while(!sl||!sr){
sl=sr=0;
if(L[i]==1)sl=1;
else{
if(L[i]<=key[L[i]-1]&&key[L[i]-1]<=R[i]){
L[i]=L[L[i]-1];sl=sr=0;
}
else sl=1;
}
if(R[i]==k)sr=1;
else{
if(L[i]<=key[R[i]]&&key[R[i]]<=R[i]){
R[i]=R[R[i]+1];sl=sr=0;
}
else sr=1;
}
}
}
int main()
{
n=read();m=read();q=read();
for(int i=1,x,y;i<=m;i++){
x=read();y=read();key[x]=y;
}
for(int i=k=1;i<=n;i++){id[i]=k;if(key[i])k++;}
for(int i=1;i
D2T2:排列
给定\(n\)个整数\(a_1,a_2,\dots,a_n,0\le ai\le n\),以及\(n\)个整数\(w_1,w_2,\dots,w_n\)。
称 \(a_1, a_2, \dots, a_n\)的 一个排列 \(a_{p[1]}, a_{p[2]}, \dots, a{p[n]}\)为 \(a_1, a_2, \dots, a_n\)的一个合法排列,
当且仅当该排列满足:
对于任意 的 \(k\) 和任意的 \(j\),如果 \(j \le k\),那么 \(a_{p[j]}\)不等于 \(p[k]\)。
(换句话说就是:对于任意的 \(k\) 和任意的 \(j\),如果 \(p[k]\)等于 \(ap[j]\),那么\(k
定义这个合法排列的权值为 \(w_{p[1]} + 2w_{p[2]} + \dots + nw_{p[n]}\)。
你需要求出在所有合法排列中的最大权值。如果不存在合法排列,输出\(-1\)。
\(n\le 5\times 10^5\)
考虑转化题意,\(a_i=k\)表示重新排列后\(a_k\)要在\(a_i\)前面,那么连一条\(k->i\)的有向边。
可以发现这样转化之后,只要能在图中选择一个合法的拓扑序,就能形成一个合法排列。
于是图中有环即无解,无环后每个点仅有\(1\)入度,形成了一棵以\(0\)为根的外向树。
问题转化为:给出一棵有点权的树,从根节点出发选择一个树的遍历顺序,第\(i\)个点经过时间为\(t\)时会给答案加上\(tw_i\)的贡献,求最大总贡献。
你可以发现树上序列\(dp\)归并是正确的,时间复杂度为\(O(n^2)\),可以得到\(60\)分。
我们知道如果一个点权值非常小,那么选择父亲后肯定优先选择它;
于是考虑贪心,每次选择一个权值最小的点,将其缩到父亲上并贡献答案,
父亲的权值变成所在节点的平均值。
具体细节可以看代码 or 别的题解...
这份\(set\)的代码不开\(O2\)是过不去的...
#include
#define FL "a"
using namespace std;
typedef long long ll;
typedef double dd;
const int N=5e5+20;
const int inf=2147483647;
const int mod=998244353;
const ll INF=1ll<<60;
inline ll read(){
ll data=0,w=1;char ch=getchar();
while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
if(ch=='-')w=-1,ch=getchar();
while(ch<='9'&&ch>='0')data=data*10+ch-48,ch=getchar();
return data*w;
}
inline void file(){
freopen(FL".in","r",stdin);
freopen(FL".out","w",stdout);
}
int n,a[N];
int f[N];int find(int x){return f[x]!=-1?f[x]=find(f[x]):x;}
int head[N],nxt[N<<1],to[N<<1],cnt;
inline void add(int u,int v){
to[++cnt]=v;nxt[cnt]=head[u];head[u]=cnt;
}
int fa[N],sz[N];ll w[N],ans;
struct node{ll val;int id;};
inline bool operator <(node a,node b){
if(a.val*sz[b.id]!=b.val*sz[a.id])return a.val*sz[b.id]S;set::iterator it1,it2;
int main()
{
n=read();memset(f,-1,sizeof(f));
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=n;i++)w[i]=read();
for(int i=1;i<=n;i++){
if(find(i)==find(a[i]))
return puts("-1"),0;
f[find(i)]=find(a[i]);
add(a[i],i);fa[i]=a[i];
}
w[0]=INF;
for(int i=0;i<=n;i++){
sz[i]=1;f[i]=-1;
S.insert((node){w[i],i});
}
while(S.size()!=1){
it1=S.begin();
int u=it1->id,ff=find(fa[u]);
it2=S.find((node){w[ff],ff});
S.erase(it1);S.erase(it2);
ans+=w[u]*sz[ff];
if(ff)w[ff]+=w[u];
sz[ff]+=sz[u];
S.insert((node){w[ff],ff});
f[find(u)]=ff;
}
printf("%lld\n",ans);
return 0;
}
D2T3:道路
dp状态为\(f[i][a][b]\),没什么好说的。
从前的码风...
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include