NOI2016~2021 Solution Set
口胡一张嘴。
28/36。
NOI2016
优秀的拆分
极限得分:95(100?)。
在 \(\text{AABB}\) 的 \(\text{AB}\) 间计数,然后相当于求 \(f_i,g_i\) 分别表示以第 \(i\) 个字符结尾有多少个 \(\text{AA}\),\(g_i\) 类似。
枚举 \(|A|\),然后在 \(|A|,2|A|,\cdots\) 放置分割点,容易发现 \(\text{AA}\) 过两个分割点。然后查 LCP 或 LCS 后缀数组解决。
网格
极限得分:92(写挂了)。
答案显然小于等于 \(2\)。
判断 \(-1,0\) 比较显然,然后现在是找整个图里面有没有割点。我们选取一些怪七八糟的关键点然后 Tarjan 就好了。主要是没有证明很麻烦,具体是如果该行有蛐蛐,那就正上下左右以及与之八联通的格子看成关键点,然后在选取所有的四个角的八联通格子当成关键点。
建图就 \(x,y\) 相同的建一块儿就好了。证明我不会,反正过掉了 =.=
循环之美
极限得分:100。
简单莫比乌斯反演。规律显然。主要是不想写式子,后面补吧。这个题在莫比乌斯反演一套里面算很简单的了。
算了写一发,理性猜测选出来的分数 \(\dfrac{i}{j}\) 要满足 \(j ⊥ k\) 且 \(j ⊥ i\),证明显然(除了相当于小数点移位了)。那就化一波式子,中间那些杂七杂八的不要了,就是:
把里面的 \([jd ⊥ k]\) 拆成 \([j ⊥ k]\) 和 \([d ⊥ k]\),并把只跟 \(d\) 有关的东西提出来。
那里面那坨东西非常好算只需要预处理 \(O(k)\) 项。已经是一个类似于整除分块的形式。把不能整除分块的东西提出来做这个。总之可以拆成一个可以递归的形式,然后上杜教筛求 \(\mu\) 的前缀和。
吐槽,这种题出出来不是给差点金哥哥送分的?
区间
极限得分:100。
大概是说因为只跟最大的和最小的有关,我们将区间长度从大到小排序,选出来的区间定然是一段。离散化后线段树维护区间的覆盖,双指针移动就好了。
国王饮水记
极限得分:85。
每次操作显然必带结点 \(1\),不然没有意义。
然后显然我们每次只能操作比当前更大的,可以得到一个 \(O(n^2kp)\) 的 DP。当然你可以暂时先不管精度,然后倒推,可以做到 \(O(n^2k)\),其中 \(k\) 的级别可以到 \(O(n)\)(不妙啊!这样就可以 85 了)。
这个 DP 不好优化,注意到每次选两个比较优,猜测选的大段个数和长度不会很大。那么先用上面不管精度最后倒推的方法,然后设个一个好的转移阈值(转移区间最大长度,之类的)就好了,总之能过。
旷野大计算
极限得分:40(吧?)。
我再也不抄题解了,对不起。这个题太牛逼了,找个时间好好做……
NOI2017
真是奇怪啊,为什么这场我是真的一个题也不会啊(划掉)
整数
极限得分:44(你妈啊,我是真不会啊)。
正负拆开,只需要比较前 \(\bold{k}\) 位大小。暴力加均摊复杂度正确,压位计算就好了。
话说得好听啊,但是我怎么觉得这个狗题这么难写??
蚯蚓排队
极限得分:100(?有点大并。)
直接大暴力就好了…… 时间复杂度是 \(O(mk^2)\) 的,哈希就好了。
读题是难点。
泳池
极限得分:25(15?)。
不会,不会就是不会,怎么学也不会。
先容斥掉,小于等于 \(K\) 的减去小于 \(K\) 的。不用大于是因为上限有点太高了。
【等待施工中……】
游戏
极限得分:100。
到达 NOI2017 最垃圾题,游戏!太垃圾啦游戏。哎呀这不 2-SAT 吗,还是枚举一下 x
是什么吧家人们。
好吧也许蚯蚓排队最垃圾,但是差不多嘛。枚举 x
之后每个位置只有两种选择…… 做做就好了。
蔬菜
极限得分:80+12(?)(部分分真的送的很多吗?纯纯的口胡),有点病,我突然觉得我能过。
显然倒推就可以直接硬贪心,这个是普及组问题。现在有多组,因为这是个普及组问题,你要从 day \(p\) 到 day \(p-1\),就是直接挑 \(m\) 个收益最小的菜不要就好了……
然后似乎有个不用倒推的并查集贪心做法,非常的 exciting 啊!
NOI2018
归程
极限得分:100。
先对水位线从大到小排序建一个 Kruskal 重构树,然后求出 \(1\) 到其他的点的最短路,然后树上倍增找到可以到达的所有点(容易发现构成一棵子树),找个最小值就好了。
冒泡排序
极限得分:72(数数水平低于人均 T^T)。
显然达到下界就是一个数不会往左移了往右移。如果出现一个长度为 \(3\) 的下降子序列就非常的不优秀(假设为 \(a,b,c\),在动 \(a\) 的时候 \(b\) 向左,动 \(b\) 的时候一定会和 \(c\) 交换)。
Dilworth 定理告诉我们最长链长度等于最大反链独立集,也就是有最多两个上升子序列。记 \(dp_{i,j}\) 表示选了 \(i\) 个数,最大值为 \(j\),那么向后要么加小于 \(j\) 的最小数,要么加大于 \(j\) 的数。
容易得到一个 \(O(n^3)\) 的 DP 转移,前缀和优化之后可以得到 \(dp_{i,j} = dp_{i-1,j}+dp_{i,j-1}\) 的转移。然后为了下面的分析,我们令 \(dp_{n, n}\) 为 \(1\),转移改为 \(dp_{i,j}=dp_{i+1,j}+dp_{i,j+1}\)。
然后注意到 \(i>j\) 的时候 \(dp_{i,j}\) 为 \(0\)。这个形式类似于网格图,每次只能向下或者向左动,不能越过 \(y=x\) 这条线。
考虑计数。我们假设前 \(i-1\) 位和 \(q\) 一样,并且选出的最大值为 \(v\),没选到的最小值为 \(u\)。
在选择第 \(i\) 位的时候,因为字典序要严格大于 \(q\),因此第 \(i\) 位的数 \(k > \max(q_i,v)\)。那么相当于有初始值 \(dp_{i,\max(q_i,v)+1 \cdots n} \gets 1\)。前缀和优化到 \(dp_{i-1,\max(q_i,v)+1} \gets 1\)。用网格图的角度就是 \((n,n) \to (i-1,\max(q_i,v)+1)\) 并且不越过 \(y=x\)。简单分析就好了,这个问题很经典。
然后如果 \(u<q_i<v\) 说明后面已经没有前途了!退出吧,再算就错了。否则显然后面还能继续填。
你的名字
极限得分:68(字符串一点也不会)。
试图建出 \(s[l\cdots r]\) 的后缀自动机。先建出 \(s\) 的后缀自动机,后缀树部分和自动机转移部分显然不变,要变的只有 endpos。
那就线段树合并表示出 endpos,然后就是近似 \(l=1,r=|s|\) 的情况,暴力匹配就好了。不难写。
另外算答案,注意到答案是 \(t\) 的本质不同子串个数,减去 \(s[l \cdots r]\) 与 \(t\) 的本质不同公共子串个数。那么保证每次减去的都是本质不同的就好了…… 也就是在第 \(i\) 个位置的时候,匹配长度为 \(p\),\(t\) 的后缀自动机上新增的结点的父亲结点表示的最大长度为 \(q\),贡献就是 \(i-\max(p,q)\)。
屠龙勇士
极限得分:100。
就是简单魔改 EXCRT,乱写就好了。
NOI2019
回家路线
极限得分:100(真的不是 O(mt) 吗)。
哦还真不是。虽然就也很,也许很简单吧,直接斜优!
好吧我过不了加强版。
序列
极限得分:64。
很久以前以为是个牛逼题,现在看比较傻…… 大概是说你先费用流建图建出来,可以直接反悔贪心做,分直接选一个 \(A_i,B_i\),退一个 \(A\) 部点往虚点 \(A\) 的流,退一个虚点 \(B\) 往 \(B\) 部点的流。容易发现每次这种操作会让相同的下标多至少一个,先选出大的 \(K-L\) 个,再做 \(L\) 次上述过程就好了。遗憾的是口胡一张嘴啊,写不出来 100。
弹跳
极限得分:100。
k-d tree 优化建图,非常板子。
斗主地
极限得分:40。
前面的暴力和正解几乎没有关系。有个牛逼结论是一次函数洗牌之后还是一次函数,二次函数洗牌之后还是二次函数……如果有研究一下样例的意识就可以把结论猜出来了。
I 君的探险
极限得分:36(正在做)。
Subtask 1, 2:直接暴力。
Subtask 3:二进制分组。
NOI2020
美食家
\(w_i\) 很小,拆点。不要拆边,拆点就好。
图建出来,走到第一个点加一点愉悦值,写成一个类似于矩阵的形式,转化成 \((\max,+)\) 的广义矩阵乘法。美食节直接枚举就好了,中间可以预处理 \(2^k\) 的矩阵。
命运
先咕一下……之后重写一遍。
制作菜品
\(n=m\) 很萌萌。
\(m=n-1\) 也很萌萌阿。
然后 \(m=n-2\) 就不可爱了。我们想转化成两个 \(m_i = n_i - 1\) 的形式这样就有解了,那就是看有没有 \(S\) 使得 \(\displaystyle \sum_{i \in S} d_i = (|S|-1)k\)。左右两边同时减 \(|S|k\) 之后就是 \(\displaystyle \sum_{i \in S} d_i-k = -k\),这是 01 背包可以 bitset
优化。
老实说我觉得这个题挺简单阿,嗯哼?
超现实树
当初有点傻逼,写的题解也很蠢蛋阿。
先分析出如果每个点两个子树的大小 \(\min > 1\) 那么这个树无用的结论(我可以缩小,然后得到你没有的东西,是个天缺)。
然后有一个小 case 是单独一个根结点,和根结点左接一个点右边不接,根结点右接一个点左边不接,两边都接一个点等价。容易发现一个点是完备的,当且仅当:
- 存在一棵有用的树其是光秃秃的一个结点挂在这里;
- 下面四个情况同时满足:
- 有一棵有用的树,其右儿子是完备的,左子树为空;
- 有一棵有用的树,其左儿子是完备的,右子树为空;
- 有一棵有用的树,其右儿子完备,左子树大小为一;
- 有一棵有用的树,其左儿子完备,右子树大小为一。
暴力判断就好了。
NOI2021
应该是自 16 年以来最简单的一套。
轻重边
看起来可以用 LCT,实际确实可以,这是解法一。
解法二是像雨兔一样现场研究毛毛虫,但是这个代价可能比较高昂。
解法三就是普通树剖,但是 motivation 比较奇怪。
注意到路径上的所有点与其连接的所有边全变轻,考虑用颜色这样一个概念去描述。一个边是重的当且仅当两点颜色相同。这样就是链染色问题,整个问题变成了一个原题叫做 SDOI2011 染色,注意处理没染色的情况。这是个经典问题,重链线段树轻边暴力记下链头是什么就好了。
路径交点
Binet-Cauchy 定理简单运用。就是一堆矩阵乘起来求个行列式。
当然也可以 LGV。
关于动机的话,研究 \(k=2\)(就是直接求行列式的 case)之后这个题就是萌萌题了。(至于为什么乘起来,可以看这个)。
庆典
先缩点。现在是个外向树。
\(k=0\) 是萌萌。
\(k=1\) 的话,\(u \to v\),\(u \to u_1 \to v_1 \to v\) 两条路径。如果说 \(u\) 在 \(v_1\) 子树内那还要多一个 \(v_1 \to u\)。可以用树剖打标记,下面都这么处理,每次清空就好。
\(k=2\) 按照上面的模式,如果存在一条 \(u\to v\) 的路径,接下来的工作就只有找环。首先如果每条路径可以通过怪七八糟的手法可以从 \(u/v\) 重复到达 \(u/v\) 那路径就是好的,然后好的路径之间还可以有环。
st=r[0]=r[1]=false;
if(In(u,v)) st=true,modifyChain(u,v);
for(int i=0;i<=1;++i)
{
if(In(u,adu[i]) && In(adv[i],v)) modifyChain(u,adu[i]),modifyChain(adv[i],v),r[i]=st=true;
if(In(u,adu[i]) && In(adv[i],adu[i^1]) && In(adv[i^1],v)) modifyChain(u,adu[i]),modifyChain(adv[i],adu[i^1]),modifyChain(adv[i^1],v),r[i]=r[i^1]=st=true;
}
if(st)
{
for(int i=0;i<=1;++i)
{
if(In(v,adu[i]) && In(adv[i],u)) modifyChain(v,adu[i]),modifyChain(adv[i],u),r[i]=true;
if(In(u,adu[i]) && In(adv[i],u)) modifyChain(adu[i],adv[i]),r[i]=true;
if(In(v,adu[i]) && In(adv[i],v)) modifyChain(adu[i],adv[i]),r[i]=true;
if(In(v,adu[i]) && In(adv[i],adu[i^1]) && In(adv[i^1],v)) modifyChain(v,adu[i]),modifyChain(adv[i],adu[i^1]),modifyChain(adv[i^1],v),r[i]=r[i^1]=true;
if(In(v,adu[i]) && In(adv[i],adu[i^1]) && In(adv[i^1],u)) modifyChain(v,adu[i]),modifyChain(adv[i],adu[i^1]),modifyChain(adv[i^1],u),r[i]=r[i^1]=true;
if(In(u,adu[i]) && In(adv[i],adu[i^1]) && In(adv[i^1],u)) modifyChain(u,adu[i]),modifyChain(adv[i],adu[i^1]),modifyChain(adv[i^1],u),r[i]=r[i^1]=true;
}
for(int i=0;i<=1;++i)
{
if(r[i] && In(adv[i],adu[i^1]) && In(adv[i^1],adu[i])) modifyChain(adv[i],adu[i^1]),modifyChain(adv[i^1],adu[i]);
if(r[i] && In(adv[i],adu[i])) modifyChain(adv[i],adu[i]);
}
}
量子通信
妙妙题,当初同步赛遗憾题。
\(k\) 很小,考虑将 \(256\) 位二进制数变成 \(16\) 位 \(2^{16}\) 进制数。
因为 \(k \leq 15\),所以如果可能在字典出现至少有一位是一样的。
因为数据随机所以期望复杂度正确啊。虽然我觉得有点小小的搞笑。
密码箱
当初我是小丑吗??
首先根本不需要管最简这个条件,因为上下一定互质。
然后是,将分母上下看成矩阵,密码箱的操作就像是在做线性变换,每次右乘一个矩阵。
然后对每个操作构造一个矩阵然后插在后面,这个矩阵非常好构造我就稍微摆了,W
是 \(\begin{bmatrix} 1 & 1 \\ 0 & 1\end{bmatrix}\),E
在两种情况下都是 \(\begin{bmatrix} 2 & -1 \\ 1 & 0\end{bmatrix}\),原来的线性变换是 \(\begin{bmatrix} a & 1 \\ 1 & 0\end{bmatrix}\)。
想不到吧,这是个构造题。
机器人游戏
首先 \(O(nm2^n)\) 的容斥写在样例里面了。
然后想平衡复杂度一脸折半。枚举钦定的最右边的指针 \(r\),不会爆炸的机器人位移只有 \(n-r\)。DP 定义为当前处理了前 \(i\) 个位置(每加一个位置容斥系数会多一个 \(-1\)),在包括当前点的长为 \(n-r\) 的范围外有没有钦定的位置(要看后面的是不是一定有一个不变在),然后最近的 \(n-r\) 个位置钦定的状态为 \(S\)。在 \(r\) 很小的时候也就 \(r2^r\),在 \(r\) 很大的时候是 \(r2^{n-r}\),这样复杂度是平衡的。
然后要考虑的是转移的系数以及 \(r\) 后面的元素。先考虑第一个问题,我们要求某个位置系数为 \(1,2,3\) 的机器人的个数(一个位置同时有状态 \(0,1\) 或 \(2,3\);一个位置同时有 \(0,1\) 中的一个或 \(2,3\) 中的一个;系数为 \(3\) 的可以算有效机器人减去用掉的)。先用 bitset
存下以任意 \(s\) 为起点,使得第 \(s+i\) 个位置的状态是 \(0/1/2/3\)(不变/变成相反/变成 \(0\)/变成 \(1\))的机器人的位置状态。
那么首先如果当前在算的位置 \(p\) 不是 \(r\)(在这里等价于小于 \(r\),因为 \(r\) 显然使得 \(p\) 不变)或者是在 \(n-r\) 范围外有钦定的点,那么当前所有的机器人都会造成一个不变,比较显然;然后是可以算在这 \(n-r\) 个位置里面,所有被钦定的点的状态,使得当前这个位置变成状态 \(0/1/2/3\) 的机器人集合。这样分类讨论取个并就好了。
然后是处理 \(r\) 之后的元素,被限制的位置同样可以采用上面的方法解决,也就是枚举离 \(r\) 的距离 \(s\),然后枚举状态算出使 \(s\) 的状态是 \(0/1/2/3\) 的机器人集合,又是一顿算就好。
这个题代码太你妈抽象了,,,,所以我贴份代码。代码很清楚阿,要仔细看看。
/*
他决定要“格”院子里的竹子。于是他搬了一条凳子坐在院子里,面对着竹子硬想了七天,结果因为头痛而宣告失败。
DONT NEVER AROUND . //
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef double DB;
typedef unsigned long long ULL;
const int MOD=1e9+7;
inline int Add(int u,int v){return u+v>=MOD?u+v-MOD:u+v;}
inline int Sub(int u,int v){return u-v>=0?u-v:u-v+MOD;}
inline int Mul(int u,int v){return LL(u)*LL(v)%MOD;}
inline int add(int &u,int v){return u=Add(u,v);}
inline int sub(int &u,int v){return u=Sub(u,v);}
inline int mul(int &u,int v){return u=Mul(u,v);}
int QuickPow(int x,int p=MOD-2)
{
if(p<0) p+=MOD-1;
int ans=1,base=x;
while(p)
{
if(p&1) ans=Mul(ans,base);
base=Mul(base,base);
p>>=1;
}
return ans;
}
inline int lowbit(int x){return x&(-x);}
inline int popcount(int x){int ret=0;while(x) ++ret,x^=lowbit(x);return ret;}
typedef bitset<1005> BT;
BT F[(1<<17)][4];
BT Cv[35][4];
BT US;
int n,m;
char str[1005][105];
int v[1005][105],len[1005],mv[1005];
int dp[35][2][(1<<17)];
int pw2[1005],pw3[1005];
vector<int> uid[100];
int main(){
scanf("%d %d",&n,&m);
for(int i=1;i<=m;++i) scanf("%s",str[i]+1),len[i]=strlen(str[i]+1);
pw2[0]=pw3[0]=1;
for(int i=1;i<=m;++i) pw2[i]=Add(pw2[i-1],pw2[i-1]),pw3[i]=Add(Add(pw3[i-1],pw3[i-1]),pw3[i-1]);
for(int i=1;i<=m;++i)
{
for(int j=1;j<=len[i];++j)
{
if(str[i][j]=='R') ++mv[i];
else if(str[i][j]=='*') v[i][mv[i]]^=1;
else if(str[i][j]=='0') v[i][mv[i]]=2;
else v[i][mv[i]]=3;
}
uid[mv[i]].push_back(i);
}
US.flip();
int ans=0,siz=0;
for(int r=n;r;--r)
{
siz+=int(uid[n-r].size());
for(auto id:uid[n-r]) for(int i=0;i<=n+2;++i) Cv[i][v[id][i]][id]=1;
memset(dp,0,sizeof dp);
dp[0][0][0]=1;
for(int p=1;p<=r;++p)
{
int Tup=1<<min(p-1,n-r+1),Sup=1<<min(p,n-r+1),U=Sup-1;
for(int c=0;c<=1;++c)
{
for(int S=0;S<Tup;++S)
{
int cn=(c || ((S<<1)>U));
if(p^r) add(dp[p][cn][(S<<1)&U],dp[p-1][c][S]); // r 被强制选。
sub(dp[p][cn][((S<<1)|1)&U],dp[p-1][c][S]);
}
}
for(int c=0;c<=1;++c)
{
int ct=c || (p^r);
for(int j=0;j<=min(n-r+1,p)-1;++j) for(int t=ct;t<=3;++t) F[(1<<j)][t]=Cv[j][t];
for(int S=0;S<Sup;++S)
{
if(S) for(int t=ct;t<=3;++t) F[S][t]=F[S^lowbit(S)][t]|F[lowbit(S)][t];
if(ct)
{
BT v1=F[S][1]|(F[S][2]&F[S][3]),v2=(F[S][2]|F[S][3])&(US^v1); // 这个时候 F[S][0] = U.
int p1=v1.count(),p2=v2.count();
mul(dp[p][c][S],Mul(pw2[p2],pw3[siz-p1-p2]));
}
else
{
BT v1=(F[S][0]&F[S][1])|(F[S][2]&F[S][3]),v2=(F[S][0]|F[S][1])&(F[S][2]|F[S][3])&(US^v1);
int p1=v1.count(),p2=v2.count();
mul(dp[p][c][S],Mul(pw2[p2],pw3[siz-p1-p2]));
}
}
}
}
int dis=min(n-r+1,r),Sup=1<<dis;
for(int c=0;c<=1;++c)
{
for(int s=1;s<=n-r;++s)
{
for(int j=0;j<dis;++j) for(int t=c;t<=3;++t) F[(1<<j)][t]=Cv[j+s][t];
for(int S=0;S<Sup;++S)
{
if(S) for(int t=c;t<=3;++t) F[S][t]=F[S^lowbit(S)][t]|F[lowbit(S)][t];
if(c)
{
BT v1=F[S][1]|(F[S][2]&F[S][3]),v2=(F[S][2]|F[S][3])&(US^v1);
/*
这个时候 F[S][0] = U.
*/
int p1=v1.count(),p2=v2.count();
mul(dp[r][c][S],Mul(pw2[p2],pw3[siz-p1-p2]));
}
else
{
BT v1=(F[S][0]&F[S][1])|(F[S][2]&F[S][3]),v2=(F[S][0]|F[S][1])&(F[S][2]|F[S][3])&(US^v1);
int p1=v1.count(),p2=v2.count();
mul(dp[r][c][S],Mul(pw2[p2],pw3[siz-p1-p2]));
}
}
}
}
for(int c=0;c<=1;++c) for(int S=0;S<Sup;++S) sub(ans,dp[r][c][S]);
}
printf("%d",ans);
return 0;
}