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\)),有

\[\large f_{i}=hit_i+\sum_{j \in T_{i}}{hit_{j}} \]

等等,其实我们可以把这个不咋和谐的 \(hit_i\)ban掉:

你主动攻击他...相当于你攻击了他自己使得他自己状态改变!(啥离谱言论啊...雷元素方碑给自己挂雷元素点亮自己)

也就是一个方碑可以影响其自身。

没毛病。我们就可以把 \(i\) 本身也塞进 \(T_i\) 里,这样的话原来的式子就变成:

\[\large f_{i}=\sum_{j \in T_i}hit_j \]

优美!...但还是不够!

还能再改

我们都把他自己塞进\(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)\)啊!

那么有

\[\begin{cases} \sum{(a_{j,1}\times hit_j)} +s_1 \equiv t_1&(mod\;\;m)\\ \sum{(a_{j,2}\times hit_j)} +s_2 \equiv t_2&(mod\;\;m)\\ \sum{(a_{j,3}\times hit_j)} +s_3 \equiv t_3&(mod\;\;m)\\ ... \end{cases} \]

\(s_i\) 挪到右边就变成典型的高斯消元啦 我一般喜欢打高斯-约旦消元2333

For不认得\(\sum\)的大佬:

\[\begin{cases} a_{1,1}\times hit_1 + a_{2,1}\times hit_2+\dots +a_{n,1}\times hit_n +s_i \equiv t_1&(mod\;\;m)\\ a_{1,2}\times hit_1 + a_{2,2}\times hit_2+\dots +a_{n,2}\times hit_n +s_i\equiv t_2&(mod\;\;m)\\ a_{1,3}\times hit_1 + a_{2,3}\times hit_2+\dots +a_{n,3}\times hit_n +s_i\equiv t_3&(mod\;\;m)\\ \dots\\ a_{1,n}\times hit_1 + a_{2,n}\times hit_2+\dots +a_{n,n}\times hit_n +s_i\equiv t_n&(mod\;\;m)\\ \end{cases} \]

也就是一楼大佬的式子喽
不知道你看着累不累反正我一行一行打是挺累的

正解

发现题目给的 \(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\)列更新

明显是\(\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;
}

题解就到这里,感谢观看,若有问题可以随时问咕


\[the\;End \]

posted @ 2026-07-20 20:51  听凭风引adj  阅读(4)  评论(0)    收藏  举报