Luogu P10315 [SHUPC] 原神,启动!- 题解
P10315 [SHUPC 2024] 原神,启动!题解
同步发表于洛谷。
前言
废话篇
骗你的稻妻这个解谜我就没打过
话说让芽衣姐上岂不是更快,一刀雷系999诶发现这个题还可以交题解,遂写此文咕咕咕
题目背景
咳咳,停停停,我在说什么呢,这可是一篇正经题解啊awa
我看上面几个大佬大多数都是"显然",就,本蒟蒻还没搞清楚怎么想出来的,怎么就正解了?So,这篇文章主要写思路,废话较多,各位大佬和管理见谅~
现在有 \(n\) 个雷元素方碑,每个方碑都有 \(m\) 种不同状态(\(0,1, ···,m-1\));
黄泉可以选择任意一个方碑进行攻击 使用 八雷飞渡,使单个方碑从 \(s\) 状态变成 \(s+1\) 状态 (特别的,若此时方碑为 \(m-1\) 状态,变为 \(0\) 状态);
同时,与它相连的 \(k_i\) 个方碑( \(a_1...a_{k_i}\))都会变成下一个状态(不会连锁),因为八雷飞渡是扩散技能,
现在,给定每个方碑的初始状态 \(s_i\),当所有方碑全部达到指定状态 \(t_i\) 时视为解谜成功;
输入格式
\[\begin{matrix} &n &m\\ &k_1 &a_1 &a_2 &\dots &a_{k_1}\\ &k_2 &a_1 &a_2 &\dots &a_{k_2}\\ &\vdots &\vdots &&&\vdots\\ &k_n &a_1 &a_2 &\dots &a_{k_n}\\\\ &s_1 &s_2 &s_3 &\dots &s_n\\ &t_1 &t_2 &t_3 &\dots &t_n \end{matrix} \]
重要的数据范围
$1 \le n \le 100,\space m \le 10^9 $且 保证 \(m\) 是素数,
\(0 \le k_i <n\),
\(1 \le a_j \le n,\;a_j \ne i\) 且 \(a_{j-1}<a_j\)(序列保证升序单调递增)
样例略。
题目分析
你也许切过关灯问题
(双倍经验)或Gambler Bo(现在Luogu还没这个题)这两个题,没切过也没关系。
遇见题不会怎么办?先看难度和标签我去青题还是高消可以切一切 不行不行,考试的时候可没有标签
当然是先打暴力啦
我们可以直接根据题意建图暴搜,但是显然会WA掉,因为不能确定某个方碑是先被攻击的
那就一点一点优化。其实当时觉得这题似乎有那么一丢丢 DP 的味道,不管了先试再说
定义:
- \(\large f_{i}\) 表示 \(i\) 方碑状态改变了几次
- \(\large hit_i\) 表示 \(i\) 被主动攻击了几次
暂时不考虑 \(m-1\) 的限制,根据题意,有
对于每一个方碑 \(i\),如果主动使 \(f_i\)+1,则所有 $ f_{s_i}$ 都会+1
但是这还是不怎么好处理状转... (总不能在一个图上跑死吧?)
而且这东西它有后效性啊,跑DP也会WA掉...
改进
我们可以 反向建边 ,原题说改变别的点,那我们反着来,另 (没打错) \(T_{i}\) 为 能够使方碑 \(i\) 状态改变的所有方碑 的集合(没学过集合也没关系,你懂 反向建边 是啥就行),这样就可以得出一个状转:
对于每个 \(j \in T_{i}\)(所有能够使 \(i\) 状态改变的 \(j\)),有
等等,其实我们可以把这个不咋和谐的 \(hit_i\) 给ban掉:
你主动攻击他...相当于你攻击了他自己使得他自己状态改变!(啥离谱言论啊...雷元素方碑给自己挂雷元素点亮自己)
也就是一个方碑可以影响其自身。
没毛病。我们就可以把 \(i\) 本身也塞进 \(T_i\) 里,这样的话原来的式子就变成:
优美!...但还是不够!
还能再改!
我们都把他自己塞进\(T_i\)里了,那把所有点都塞进去也不是不行,只不过有一些点明明就是不会影响到 \(i\)...
那就赋给每一个 \(hit_j\) 一个 系数 \(a_{j,i}\):
- 如果这个点 \(j\) 能影响到 \(i\),那这个 \(hit_j\)就可取,\(a_{j,i}=1\);
- 否则,\(j\) 不能影响到 \(i\),那么 \(a_{j,i}=0\)就可以把不应该取的 \(hit_j\) ban掉。
诶诶诶,我是不是发现了什么不得了的东西?!
这个 \(a_{j,i}\) 就是连的边啊!
那这样玄幻的 \(T_i\) 就毛用没有了狸!
吾去汝不早言!!
我们还注意到,刚刚我们忽略了状态 \(m-1\) 的限制和初始状态 \(s\),而他指明 \(m\) 一定是素数而且 状态从0到 \(m\).
聪明的你也许发现了,这很明显有取模的气息啊!
那么,一个节点(方碑)的最终状态就是 \(f_i+s_i\;\;(mod\;\; m)\)
也就是说,他最后要求状态 \(t_i\),就是 \(t_i=f_i+s_i\;\;(mod\;\;m)\)啊!
那么有
把 \(s_i\) 挪到右边就变成典型的高斯消元啦 我一般喜欢打高斯-约旦消元2333
For不认得\(\sum\)的大佬:
也就是一楼大佬的式子喽
不知道你看着累不累反正我一行一行打是挺累的
正解
发现题目给的 \(n\) 很小,同时还有上面这坨系数方程组,每个节点都有一个操作次数 \(hit_i\) 作为 未知数(元),我们就可以考虑 高斯消元 这个高级算法
以防你没学过高斯消元(oi wiki)
由于\(\equiv\)两侧也是可以移项的,把 \(s_i\) 移到右边就可以啦,之后就是一个典型的高斯消元狸
矩阵长这样:
\[ \begin{bmatrix} a_{1,1}&a_{2,1}&a_{3,1}&\dots&a_{n,1} &| &s_1-t_1\\ a_{1,2}&a_{2,2}&a_{3,2}&\dots&a_{n,2} &| &s_2-t_2\\ a_{1,3}&a_{2,3}&a_{3,3}&\dots&a_{n,3} &| &s_3-t_3\\ \vdots &\vdots&\vdots&\ddots &\vdots & &\vdots\\ a_{1,n}&a_{2,n}&a_{3,n}&\dots&a_{n,n} &| & s_n-t_n\\ \end{bmatrix} \]有点丑大家凑合看吧
注意:
- 这个矩阵是在 \(mod\;m\)意义下的,在消元时一定要记得随时取模,除法不能直接模,记得写乘法逆元!
- 这里矩阵里的 \(a_{j,i}\) 刚刚好把行和列换过来了欧!!!在输入时一定要注意
以防你没学过乘法逆元(教练的blog)
然后就没啥了,就一个\(\mathcal{O(n^3)}\)的高斯消元或者高斯约旦消元+\(\mathcal{O(nlogm)}\)乘法逆元...
浅证时间复杂度
\(O(n^3+nlogm)\)
是的楼上大佬 @sLMxf 时间复杂度算大了代码也错了我有写评论多有冒犯还请见谅
不加标点的长难句+1
高斯消元没的说,就是:
- 遍历矩阵 \(i\) 行(方程)找主元
- 遍历其他 \(j\) 行消元
- 遍历\(j\)行的\(k\)列更新
- 遍历其他 \(j\) 行消元
明显是\(\mathcal{O(n^3)}\)
乘法逆元:
- 遍历每一行都会除以主元(系数化为一),一共\(n\) 个主元
- 费马小定理快速幂\(O(logm)\)求逆元
明明就是 \(O(nlogm)\) 啊咕。
关于无解
如果在处理过程中,发现某一行的元全部消掉但是等式右侧没有归为零的情况显然无解,需要通知米哈游输出 niuza(牛杂)
关于无穷多解
在处理过程中,若有一个元能被完全消去,即出现化简之后某一个 \(hit_i\) 系数为零的情况说明这个元能取任意值。但是这显然不是无解的情况。题目要求输出任意一组解,所以把这个值取为任意值即可 (要不让刻师傅歇一会设为0吧...)
提示tips
切记: 十年OI一场空,__ __ ____ ____ __ __ __
AC Code
//let-the-wind-guide-you@outlook.com
%:include<bits/stdc++.h>//这个能过欧~可以去搜一搜,我当无害防伪用了
#define int long long//不开longlong见祖宗
using namespace std;
const int N=105;
int n,mod;//mod就是m
int K[N][N];//习惯用K表示系数矩阵,似乎不少人用a
void de(){//没用懒得删的debug调试咕
cout<<"_______________\n";
for(int i=1; i<=n; i++){
for(int j=1; j<=n+1; j++){
cout<<K[i][j]<<" ";
}
cout<<endl;
}
}
int ksm(int x, int k){//极简版子快速幂
int ans=1;
while(k){
if(k&1) ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
int inv(int x){//费马小定理求逆元
return ksm(x,mod-2)%mod;//一定要模,逆元也可能炸
}
void gas_jod(){//高斯-约旦消元(气体约旦bush)
for(int i=1; i<=n; i++){//循环遍历主元
int t=i; //记录用
for(int j=i; j<=n; j++){//不可以往上找!上面的要留下来作答案
if(abs(K[t][i])<abs(K[j][i])) t=j;//寻找一个绝对值最大的系数
}
if(t!=i) swap(K[t],K[i]);//直接换
if(!K[i][i]) continue; //若当前元为自由元,跳过即可,最后答案会留为0(没留为零就是无解)
int div=inv(K[i][i]); //计算乘法逆元
for(int j=1; j<=n+1; j++){
K[i][j]=(K[i][j]*div)%mod;
//实际上是算K[i][j]/div%mod,即系数化为一
}
for(int j=1; j<=n; j++){ //循环每一个要消元的行,因为是高斯-约旦要遍历所有
if(i==j)continue;
int tmp=K[j][i]; //可以理解为消元的倍数
for(int k=1; k<=n+1; k++){//循环 消元行的 所有系数
K[j][k]=((K[j][k]-tmp*K[i][k]%mod)%mod +mod)%mod;
//两式作差消去一元,一定要及时取模以及处理减法咕
}
}
}
}
int t[N],s[N];//临时存一下,也用不着
signed main(){
ios::sync_with_stdio(0);//关闭同步读入读出流
cin.tie(0); cout.tie(0);//这东西肥肠好用!
cin>>n>>mod; //输入
for(int i=1; i<=n; i++){
int k;cin>>k;
K[i][i]=1; //别忘了他自己是可以影响自己的
for(int j=1; j<=k; j++){
int num;cin>>num;
K[num][i]=1;//记得要反着写,矩阵是歪的
}
}
for(int i=1; i<=n; i++) cin>>s[i];
for(int i=1; i<=n; i++){
cin>>t[i];//读入
K[i][n+1]=((t[i]-s[i])%mod+mod)%mod;//计算恒等式右边的结果
}
gas_jod();//让我猜猜是谁写了函数没调用?
//怎么是我啊咕
for(int i=1; i<=n; i++){//检查是否无解
if(!K[i][i]&&K[i][n+1]){
cout<<"niuza\n";
return 0;
}
}
//因为之前可能跳过了某一个行,导致那一行的元没有消为零,所以要多除一次
for(int i=1; i<=n; i++){
cout<<K[i][n+1]/K[i][i]<<" ";
//反正如果消得对也是除以1没什么影响嘛
}
return 0;
}
题解就到这里,感谢观看,若有问题可以随时问咕

浙公网安备 33010602011771号