牛客每日一题

质数取石子游戏

题目思路:博弈论
下定义
首先由策梅洛定理知在最优策略下对于一个状态要么必胜要么必败
那么对于这道题定义0为必败
必胜态:如果存在一种操作能把必败态留给对手 → 当前局面是必胜态
必败态:如果所有操作都只能把必胜态留给对手 → 当前局面是必败态
(递归定义着一块QAQ)
首先打表看规律,1,2,3必胜,4必败。
发现貌似是4是一个周期,即4k必败其余必胜
以下采取归纳证明
首先1,2,3必胜,0,4必败。
假设对于\(n < N\)时4的倍数的是必败态,其余是必胜态
下证\(n = N\)(或者说其余方式也可,只要递推能跑就行)时也满足
所以分类
· \(n = 4k\)时证明其是必败态,对于所有操作,4k - p\(他一定不是4的倍数,且在\)n < N$的区间内是必胜态,所以证毕。
· \(n = 4k - 1\)\(4k - 1 - 3 = 4(k - 1)\)是4的倍数,且在\(n < N\)的区间内是必败态,所以证毕。
·\(n = 4k - 2\)\(n = 4k - 3\)时同上。
综上所述,代码如下

#include <iostream>
using namespace std;
using ll = long long;
void solve()
{
   ll n;cin >> n;
   cout << (n & 3 ? "Alice\n" : "Bob\n");
}
//常见归纳法
//sqrt(a_i + 2) = a_{i + 1}
//a_1 = 0;
//a_i <= 2
//证明a_{i + 1} <= 2

//定义0必败

//已知4必败,1, 2 ,3必胜
//假设对于x< n都有:
//4k必败,4k - 1, 4k - 2, 4k - 3必胜
//(比起寻常的只看n和n+1会更好一些)
//证明x == n的时候也满足这个
//对于4k
//你拿4k - 1, 4k - 2....4k - p以后都是非4倍数,必败
//对于4k - 1,有4k - 1 - 3,可以让其变成倍数,必胜
//对于4k - 2,和4k - 3同理必胜
int main()
{
   cin.tie(0)->ios::sync_with_stdio(false);
   int _;cin >> _;
   while(_ -- ) solve();
}

阿宁睡大觉

题目思路:指针贪心,无非就是想要最大化删除收益,我们可以跳过前导z和前导Z然后在记下z的连续区间,之后根据区间长度排序去贪心,然后用差分去标记删除区间即可。
代码如下

#include <bits/stdc++.h>
using namespace std;
using pii = pair<int ,int>;
int main() 
{
    int n, k, res{};cin >> n >> k;
    string s;cin >> s;
    while(s.size() && s.back() == 'z') s.pop_back(); 
    vector<pii>rg;
    int l = -1, r = 0;
    while(r < s.size() && s[r] == 'z') r ++;
    while(r < s.size() && s[r] == 'Z') r ++;
    l = r;
    while(r < s.size())
    {
        if(r && s[r] == 'Z')
        {
            rg.emplace_back(make_pair(l, r - 1));
            while(s[r] == 'Z') r ++;
            l = r;
        }
        else r ++;
    }
    if(l < s.size()) rg.emplace_back(make_pair(l, s.size() - 1));
    sort(rg.begin(),rg.end(),[&](pii& x, pii& y)->bool
    {
        return (x.second - x.first + 1) < (y.second - y.first + 1);
    });

    // for(int i =0 ; i < rg.size() ;i ++ ) cout << rg[i].first << " " << rg[i].second << '\n';

    vector<int>st(s.size() + 3, 0);
    for(int i = 0 ; k && i < rg.size() ; i ++ )
    {
        if(k >= rg[i].second - rg[i].first + 1)
        {
            st[rg[i].first] ++;
            st[rg[i].second + 1] --;
            k -= rg[i].second - rg[i].first + 1;
        }
    }
    for(int i = 0 ;i < s.size() + 1; i++) st[i] += (i - 1 >= 0 ? st[i - 1] : 0);
    string ss;
    for(int i = 0, fl = 1;i < (int)s.size(); i ++)
    {
        if(fl && s[i] == 'z') continue;
        else fl = 0;
        if(st[i]) continue;
        ss += s[i];
    }
    for(int i = 1 ;i < ss.size() ; i ++)
    {
        int x = (ss[i] == 'z' ? 0 : 2), y = (ss[i - 1] == 'z' ? 0 : 2);
        res += x * y;
    }
    // cout << "TEST : " << ss << '\n';
    cout << res << '\n';
}

清楚姐姐的布告规划

题目思路:01背包\(dp\)
定义\(dp_i\)为前缀长度为i的区间覆盖最小代价
转移方程(以此保证拼接无缝无重叠)即为\(dp[0] = 0\),\(dp[i] = min(dp[i], dp[i - a[1]], ... ,dp[i - a[j]]) + 1(j <= i)\)
并且他还要求我们布告一定要包含j点即对于我们使用的布告\(a[j]\)占据区间\((i - a[j], i]\),一定满足\(i - a[j] < j \le i\)
同时还要满足\(i - a[j] >= 0\)
提问:这里有可能会重复使用同一个布告吗?
不可能因为这里给定约束\(a[j]\)布告一定要在要包含\(j\)这个位置所以一旦重复使用必定会存在重合,我们之前已经排除了重合
代码如下:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 1e18; 
void solve()
{
	int n;cin >> n;
	vector<ll>a(n + 1, 0), dp(n + 1, INF); for(int i = 1 ;i <= n; i ++ ) cin >> a[i];
	dp[0] = 0;
	for(int i = 1 ;i <= n ;i ++ )
	{
		//左端点 < j ≤ 右端点
		for(int j = 1 ;j <= i ;j ++ )
		{
			if( i - a[j] < j && i - a[j] >= 0) 
				dp[i] = min(dp[i], dp[i - a[j]] + 1);
		}	
	} 
	cout << (dp[n] == INF ? -1 : dp[n]) << '\n';
}
int main()
{
	int _ ;cin >> _;
	while( _ -- ) solve();
	return 0;
}

灵梦的字符串问题

题目思路:这里的字典序定义特殊,把原本的定义倒过来了,定义该字符串字母相同的连续子串为S,所以我们发现只要有\(S_i < S_{i + 1}\)那么把\(S_i\)中内容取复制一遍字典序一定减小

一开始想的有点歪QAQ,去处理单个字符的关系了(就是先把不需要处理的结尾掐掉(即为单调不增的后缀),然后剩余的一定满足对于字符ch他的右边一定存在一个比他大的字符c,只要满足关系\(a_i \le a_{i + 1}\)即可复制(最后一个一定可以复制,如果代价允许),然后就是想一下对于连续相同字母字串的代价选取问题了(代价从小到大去选),但比较麻烦所以放弃了(详细见代码尾部注释)
代码如下

#include <bits/stdc++.h>
#include <cmath>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
const int N = 2e5 + 10;
pii stk[N];
int tt = -1;
int main() {
    cin.tie(0)->ios::sync_with_stdio(false);
    ll n, m;cin >> n >> m;
    string s;cin >> s;
    vector<int>a(s.size());for(int i = 0 ;i < s.size() ; i++ ) cin >> a[i];
    for(int i = 0 ;i < s.size() ;i ++ )
    {
        if(tt == -1 || stk[tt].first != s[i] - 'a') stk[++ tt] = make_pair(s[i] - 'a', 1);
        else stk[tt].second ++;
    }
    string res;
    // cout << "TEST : " << stk[0].second << '\n';
    for(int i = 0,lc = 0 ;i <= tt ; i++ )
    {
        if(m > 0 && i + 1 <= tt && stk[i].first < stk[i + 1].first)
        {
            priority_queue<int, vector<int>, greater<int> >q;
            int cnt = 0;
            for(int j = lc; j < lc + stk[i].second ;j ++ ) q.emplace(a[j]);
            // cout << "TEST : " << i << " " << lc + stk[i].second << '\n';
            while(!q.empty() && m - q.top() >= 0)
            {
                m -= q.top();
                q.pop();
                cnt ++;
            }
            // cout << "TEST : " << cnt << '\n';
            for(int j = 0 ;j < cnt ;j ++ ) res += (stk[i].first + 'a');
        }
        for(int j = 0 ;j < stk[i].second ; j ++ ) res += (stk[i].first + 'a');
        lc += stk[i].second;
    }
    // cout << "TEST : " << tt << '\n';
    cout << res << '\n';
}
// int n, m;cin >> n >> m;
// string s, res;cin >> s;
// vector<int>w(n);for(int i = 0; i< n ;i ++) cin >> w[i];
// string t;int lc = -1;
// for(int i = (int)s.size() - 1; i >= 0 ;i -- )
// {
//     if(t.empty() || t.back() <= s[i]) t.push_back(s[i]);
//     else
//     {
//         lc = i;break;
//     }
// }
// string ss;for(int i = 0 ;i <= lc ; i++ ) ss.push_back(s[i]);

// // cout << "TEST : " << ss << '\n';
// for(int i = 0 ;i < ss.size() ;i ++ )
// {
//     if(i + 1 < ss.size() && s[i] <= s[i + 1] && m - w[i] >= 0)
//     {
//         res += s[i];
//         m -= w[i];
//     }
//     if(i + 1 == ss.size() && m - w[i] >= 0) res += s[i];
//     res += s[i];
// }
// // cout << res <<'\n';
// for(int i = (int)t.size() - 1; i >= 0 ;i -- ) res.push_back(t[i]);
// cout << res << '\n';

区间 (interval)

题目思路:差分然后加到原数组上即可

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() 
{
    cin.tie(0)->ios::sync_with_stdio(false);
    ll n, M;cin >> n >> M;
    vector<ll>a(n + 3, 0), pre(n + 3, 0);
    for(int i = 1 ; i <= n ; i ++ ) cin >> a[i];
    while(M -- )
    {
        ll q, l, r, p;cin >> q >> l >> r >> p;
        if(q == 1) pre[l] -=p, pre[r + 1] +=p;
        else pre[l] += p , pre[r + 1] -= p; 
    }
    ll l, r, res = 0;cin >> l >> r;
    for(int i= 1; i <= n ;i ++ ) pre[i] += pre[i - 1];
    for(int i = 1 ; i <= n ;i ++ ) a[i] += pre[i];
    for(int i = l ;i <= r ; i ++ ) res += a[i];
    cout << res << '\n';
}

小辰刚学 gcd

题目思路:gcdtrick+栈合并

#include <bits/stdc++.h>
using namespace std;
int lg(int n){return 31 - __builtin_clz(n);}//注意这个只能用int
int main() 
{
    cin.tie(0)->ios::sync_with_stdio(false);
    int n, m;cin >> n >> m;
    vector<int>a(n);for(int i = 0 ;i < n ; i ++ ) cin >> a[i];
    vector<vector<int>>st(n + 10,vector<int>(35, 0));
    for(int i = 0 ;i < n ; i ++ ) st[i][0] = a[i];
    for(int i = 1; (1LL << i) <= n ;i ++ )
    {
        for(int j = 0 ; j < n  && j + (1LL << i) <= n;j ++ )
        {
            st[j][i] = __gcd(st[j][i-1], st[j + (1LL << (i - 1))][i - 1]);
        }
    }
    auto query =[&](int l,int r)->int
    {
        int k = lg(r - l + 1);
        return __gcd(st[l][k], st[r - (1 << k) + 1][k]);
    };
    auto find = [&](int x, int l, int r, int end)->int
    {
        while(l < r)
        {
            int mid = l + r + 1 >> 1;
            if(query(mid, end) < x) l = mid;
            else r = mid - 1;
        }
        if(query(l, end) >= x) return -1;
        return l;
    };
    for(int op = 0; op < m; op ++ )
    {
        int L, R, res = 0;cin >> L>> R;
        L --, R -- ;
        int end = R, cur = a[R];
        while(L <= R)
        {
            res ++;
            if(cur == 1) break;
            R = find(cur, L, R, end);
            if(L <= R) cur = query(R, end);
        }
        cout << res << '\n';
    }
}

括号匹配问题

题目思路:区间dp
重点思想 :一个序列的所有区间即为这个序列的所有后缀区间的并集(且每个后缀区间之间没有交集)
一般来说后缀(前缀)区间的继承要比单纯区间好搞一些
这里显然是后缀方便转移,那么我们定义\(dp[i]\)为以i为后缀的子区间的权值和
我们首先用一个栈来维护上一个最近的为被匹配的左括号的位置
假设\(dp[i - 1]\)已被处理
那么如果\(s[i]\)是左括号,\(dp[i] = dp[i - 1]\),并且直接入栈\(i\)
如果\(s[i]\)是右括号,在栈为空的时候无匹配发生仍然是\(dp[i] = dp[i - 1]\),如果有匹配发生那么他会影响的即为左端点为:从右往左看第一个左括号的位置,到1的所有后缀序列,他们的长度+1。
即:\(dp[i] = dp[i - 1] + stk.top()\)
最后求出每一个位置的\(dp[i]\)然后求和就是答案的一半(因为我们定义长度为一对括号,但是题目要求的是括号数量所以直接乘2)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
    cin.tie(0)->ios::sync_with_stdio(false);
    string s;cin >>s;
    ll n = s.size();
    s = '&' + s;
    vector<ll>dp(s.size() + 10 , 0);
    vector<ll>stk;
    //想到所有区间可以拆成所有后缀区间的无交集并集
    //dp[i]定义为以所有以i结尾的后缀区间的权值和
    for(int i = 1 ;i <= n ; i ++ )
    {
        if(s[i] == '(')
        {
            stk.emplace_back(i);
        }
        else if(stk.size())//有可以匹配的
        {
            
            dp[i] += stk.back();
            stk.pop_back();
        }
        dp[i] += dp[i - 1];
    }
    ll res = 0;
    for(int i =1 ;i <= n ; i++ ) res += dp[i];
    cout << res*2 << '\n';//注意最后统计出来的只是合法扩号对数
}

数楼梯

题目思路:类斐波那契数列转移式:\(dp[i] = dp[i-1] + dp[i - 2]\).初始化位置\(dp[1] = 1, dp[2] = 2\).

#include <iostream>
using namespace std;
using ll = long long;
const ll p = 998244353;
int main() 
{
    int n;cin >> n;
    ll cur = 1, pre = 1;
    for(int i = 0 ; i < n - 1; i ++ )
    {
        ll t = (cur + pre) % p;
        pre = cur; cur = t;
    }
    cout << cur % p << '\n';
}

时津风的资源收集

题目思路:bfs搜索
对于这道题首先想到bfs去暴搜所有情况复杂度为\(290^4 * 1e5\)显然会被T,然后想到其实不用四维所有情况都是平行的,所以单独搜索一个情况即可,复杂度为 \(290*4 * 1e5 = 1e8\)约为1e8理论上可以过但是加上开内存之类的时间还是被T了,最后不难想到对于这道题T可以看作询问那么无非就是预处理一下每一个终点对应的最少操作步,最终询问复杂度就是O(1)了,即位\(290 + 1e5\)显然能过
AC代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;
ll dis[333];
void init()
{
    for(int i = 0 ;i < 310 ;i ++ ) dis[i] = 1e10;
    auto in = [&](ll v)->bool
    {
        return v <= 300 && v >= 10;
    };
    auto bfs =[&]()->void
    {
        queue<pll>q;q.emplace(make_pair(10, 0));
        while(!q.empty())
        {
            pll t = q.front();q.pop();
            if(dis[t.first] != 1e10) continue;
            dis[t.first] = t.second;
            ll ne[] = {t.first + 1, t.first - 1, t.first + 10, t.first - 10,t.first + 100, t.first - 100, 10, 300};
            for(ll v : ne)
            {
                if(!in(v)) continue;
                q.emplace(make_pair(v, t.second + 1));
            }
        }
    };
    bfs();
}
int main() 
{
    cin.tie(0)->ios::sync_with_stdio(false);
    init();//预处理300~10内每一个的最少步数
    int _ = 1; cin >> _;
    while( _ -- )
    {
        array<ll, 4>S;cin >> S[0] >> S[1] >> S[2] >> S[3];
        cout << dis[S[0]] + dis[S[1]] + dis[S[2]] + dis[S[3]] << "\n";
    }
}

一开始想到的被T的4次并行搜索QAQ

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;
void solve()
{
    array<int, 4>S, T;
    for(int i = 0 ;i < 4 ;i ++ ) S[i] = 10;
    for(int i = 0 ;i < 4 ;i ++ ) cin >> T[i];
    auto in =[&](int x)->bool
    {
        return x >= 10 && x <= 300;
    };
    auto bfs = [&]()->ll
    {
        ll res = 0;
        for(int k = 0; k < 4 ;k ++)
        {
            ll v = S[k];
            queue<pll>q;q.emplace(make_pair(v, 0));
            vector<bool>vis(301, false);
            while(!q.empty())
            {
                pll t = q.front();q.pop();
                if(vis[t.first]) continue;
                vis[t.first] = true;
                if(t.first == T[k])
                {
                    res += t.second;
                    break;
                }
                ll nxt[] = {t.first + 1, t.first - 1, t.first + 10, t.first - 10,t.first + 100, t.first - 100, 300, 10};
                for(ll u : nxt)
                {
                    if(!in(u)) continue;
                    q.emplace(make_pair(u, t.second + 1LL));
                }
            }
        }
        return res;
    };
    cout << bfs() << '\n';

}
int main() 
{
    cin.tie(0)->ios::sync_with_stdio(false);
    int _ = 1; cin >> _;
    while( _ -- ) solve();
}

三角尼姆

题目思路:写几个看规律QWQ

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;

void solve()
{
    int n;cin >> n;
    if((n / 2 + n % 2) & 1) cout << "Alice\n";
    else cout << "Bob\n";
}
//0 win
//1 fail
//2 fail
//3 win
//4 win
//5 fail
//6 fail
int main() 
{
    cin.tie(0)->ios::sync_with_stdio(false);
    int _;cin >> _;
    while( _ -- ) solve();
}

跳跳跳

题目思路:区间dp
首先这道题给出限制:每个点只能跳一次且只能左右跳最近的,然后不难发现最后跳出来的即位一个连续区间,那么我们就可以直接用区间dp去维护每一个区间的最优值。
先思考初始化首先区间长度为1的点点最优值我们是知道的就是对应的\(a[i]\)
然后转移直接根据区间的左右去取即可即:
\(dp[l][r] = max(dp[l][r], dp[l + 1][r] + a[l], dp[l][r - 1] + a[r])\)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;
void solve()
{
    ll n, res{};cin >> n;
    vector<vector<ll>>dp((n + 1) << 1, vector<ll>((n + 1) << 1,0LL));
    vector<ll>a((n + 1) << 1);
    for(int i =0 ;i < n ;i ++ )
    {
        cin >> a[i];
        dp[i + n][i + n] = dp[i][i] = a[i + n] = a[i];
    }
    for(int len = 2; len <= n ; len ++ )
    {
        for(int i = 1 ;i <= (n << 1) - len + 1 ;i ++ )//所有区间左端点
        {
            int j = i + len - 1;
            dp[i][j] = max({dp[i][j], dp[i][j - 1] + a[j] * len, dp[i + 1][j] + a[i] * len});
        }
    }
    for(int i = 1 ;i <= n; i ++ ) res = max(res, dp[i][i + n - 1]);
    cout << res << '\n';
}
int main() 
{
    cin.tie(0)->ios::sync_with_stdio(false);
    int _ = 1;
    while( _ -- ) solve();
}

然后我们再思考一步空间优化
对于这里的区间我们发现其实对于长度和左端点已知的区间,那么从长度少1的区间更新过来其实不用一定要记下L和R,我们可以定义\(dp[i]\)为以i为左端点长度为上一次更新的区间的最优值(更有迭代味道了QWQ),然后最终就可以优化为
(emmmm貌似是一种区间dp的优化技巧不同于滚动数组,这里是区间dp专用的好像QWQ,虽然本质还是滚动数组)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;
void solve()
{
    ll n, res{};cin >> n;
    vector<ll>dp((n + 1) << 1);
    vector<ll>a((n + 1) << 1);
    for(int i =0 ;i < n ;i ++ )
    {
        cin >> a[i];
        dp[i + n] = dp[i] = a[i + n] = a[i];
    }
    for(int len = 2; len <= n ; len ++ )
    {
        for(int i = 1 ;i <= (n << 1) - len + 1 ;i ++ )//所有区间左端点
        {
            int j = i + len - 1;
            // dp[i][j] = max({dp[i][j], dp[i][j - 1] + a[j] * len, dp[i + 1][j] + a[i] * len});
            dp[i] = max(dp[i] + a[i + len - 1] * len, dp[i + 1] + a[i] * len);
        }
    }
    for(int i = 1 ;i <= n; i ++ ) res = max(res, dp[i]);
    cout << res << '\n';
}
int main() 
{
    cin.tie(0)->ios::sync_with_stdio(false);
    int _ = 1;
    while( _ -- ) solve();
}

企图栈贪心但是失败了

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;
void solve()
{
    ll n, res{};cin >> n;
    vector<ll>a(n << 1);
    vector<int>stkl, stkr;
    for(int i = 0 ;i < n ; i ++ ) 
    {
        cin >> a[i];
        a[i + n] = a[i];
    }
    auto update = [&]()->void
    {
        vector<bool>vis(2 * n, 0);
        ll w = 1, cres = 0;
        while(!(stkl.empty() && stkr.empty()))
        {
            while(!stkl.empty() && (vis[stkl.back()] || vis[(stkl.back() >= n ? (stkl.back() - n ): (stkl.back() + n))])) stkl.pop_back();
            while(!stkr.empty() && (vis[stkr.back()] || vis[(stkr.back() >= n ? (stkr.back() - n ): (stkr.back() + n))])) stkr.pop_back();
            if(!stkl.empty() && !stkr.empty())
            {
                if(a[stkl.back()] < a[stkr.back()])
                {
                    cres += w * a[stkl.back()];
                    if(stkl.back() >= n) vis[stkl.back()] = vis[stkl.back() - n] = true;
                    else vis[stkl.back()] = vis[stkl.back() + n] = true;
                    stkl.pop_back();
                    w ++;
                }
                else 
                {
                    cres += w * a[stkr.back()];
                    if(stkr.back() >= n) vis[stkr.back()] = vis[stkr.back() - n] = true;
                    else vis[stkr.back()] = vis[stkr.back() + n] = true;
                    stkr.pop_back();
                    w ++;
                }
            }
            else if(!stkl.empty())
            {
                cres += w * a[stkl.back()];
                if(stkl.back() >= n) vis[stkl.back()] = vis[stkl.back() - n] = true;
                else vis[stkl.back()] = vis[stkl.back() + n] = true;
                stkl.pop_back();
                w ++;
            }
            else if(!stkr.empty())
            {
                cres += w * a[stkr.back()];
                if(stkr.back() >= n) vis[stkr.back()] = vis[stkr.back() - n] = true;
                else vis[stkr.back()] = vis[stkr.back() + n] = true;
                stkr.pop_back();
                w ++;    
            }
        }
        res = max(cres, res);
    };
    for(int i = 0 ; i < n ;i ++ )
    {
        for(int j = 0; j < i ; j ++) stkl.emplace_back(j);
        for(int j = 2 * n - 1 ; j >= i ; j -- ) stkr.emplace_back(j);
        update();
    }
    cout << res << '\n';
}
int main() 
{
    cin.tie(0)->ios::sync_with_stdio(false);
    int _ = 1;
    while( _ -- ) solve();
}

【模板】非质模数下的乘法逆元

题目思路:exgcd + 裴蜀定理(即exgcd求逆元)
裴蜀定理:对于不全为0的整数a和b一定存在整数x和y使得\(ax+by = gcd(a, b)\)
定理简单证明:
先记集合D = {ax + by|x,y属于D 且 ax + by > 0},C被包含在这个集合中。而根据自然数良序定理可知非空正数集必定存在最小值,我们记做d,接下来证明d | a并且d | b。
用反证法假设d不能整除a,则\(a = qd+ r\),(0< r < d)
\(r = a - qd = a - q(ax + by) = a(1 - qx) + b(-yq)\)所以r也是a和b的线性组合也在D里,而d是D中最小所以一定有\(d \le r\)但是之前假设\(d > r\)矛盾了,所以d可以整除a,同理d可以整除b。
所以d为a和b的公约数,所以gcd(a, b) | d,所以\(gcd(a ,b) \le d\)因为gcd(a,b) | a, gcd(a,b) | b,所以根据整除性质一定可以写成,ax + by定形式,所以gcd(a, b)一定在集合D中,有\(d \le gcd(a ,b)\)
所以d = gcd(a ,b)
得证
拓展:\(ax + by = C\)有解是C为gcd(a , b)的整数倍的充要条件(用于解线性同余方程)
必要性:因为gcd(a,b) | a, gcd(a,b) | b,x和y为不全为0的整数,所以必有gcd(a, b) | c
充分性:C为gcd(a , b)的整数倍即\(C = k*gcd(a,b)\)
根据裴蜀定理必有\(ax+by = gcd(a,b)\)即有\(akx+bky = kgcd(a,b)=C\)因为\(kx\)\(ky\)也是整数满足解条件所以也有也可写成\(ax_0+by_0 = C\)即C有解
得证

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void exgcd(ll a,ll b, ll&x ,ll&y)
{
    //ax + by = gcd(a, b) = gcd(b, a % b) = bx1 + (a % b)y1
    //a % b == a - a / b * b
    //bx1 + (a - (a / b )* b)y1
    //a(y1) + b(x1 - (a/b) * y1);
    //所以x = y1, y = x1 - (a/b) * y1;
    if(!b)
    {
        x = 1, y = 0;
        return;
    }
    ll x1 = 0, y1 = 0;
    exgcd(b, a % b, x1, y1);
    x = y1, y = x1 - (a/b) * y1;
}
void solve()
{
    ll a ,m;cin >> a >> m;
    //计算a*x = 1(mod m) x即位a的逆元
    //即a*x + my = 1 (mod m)
    //根据裴蜀定理一定存在整数x和y使得ax + by = gcd(a, b)
    //并且ax + by = C有解的充要条件为C是gcd(a, b)的整数倍
    //因此要a*x + my = 1 (mod m)有解当且仅当1是gcd(a, m)的整数倍
    //即位gcd(a,m)固定为1
    if(__gcd(a ,m) != 1)
    {
        cout << "无解QAQ\n";
        return;
    }
    ll res{}, t{};exgcd(a, m, res, t);
    //注意这里求解出来的存在解x可能是负数
    //比如3*(-7) + 11 * 2 == 1
    //所以我们要取个mod
    cout << (res % m+ m) % m << '\n';
}
int main() 
{
    cin.tie(0)->ios::sync_with_stdio(false);
    int _ ;cin >> _;
    while( _ -- ) solve();
}

其他的应用在解线性同余方程如:
P1082 [NOIP 2012 提高组] 同余方程

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void exgcd(ll a,ll b, ll&x ,ll&y)
{
    if(!b)
    {
        x = 1, y = 0;
        return;
    }
    ll x1 = 0, y1 = 0;
    exgcd(b, a % b, x1, y1);
    x = y1, y = x1 - (a/b) * y1;
}
void solve()
{
    ll a,m;cin >> a >> m;
    ll x , y;exgcd(a, m, x, y);
    x = (x % m + m) % m;
    cout << x << '\n';
}
int main() 
{
    cin.tie(0)->ios::sync_with_stdio(false);
    int _ = 1;
    while( _ -- ) solve();
}
// 64 位输出请用 printf("%lld")

数字游戏

题目思路:暴力模拟

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll lg(ll x){return 63 - __builtin_clzll(x);}
void solve()
{
    ll x, cnt{};cin >> x;
    while(x)
    {
        if(__builtin_popcountll(x) & 1) x ^= 1;
        else x -= 1LL << lg(x);
        cnt ++;
    }
    cout << cnt << '\n';
}
int main() 
{
    cin.tie(0)->ios::sync_with_stdio(false);
    int _ ;cin >> _;
    while( _ -- ) solve();
}

迷宫可达任务

题目思路:tarjan找scc(强联通分量)化简图 + 互相可达图性质
tarjan算法介绍QWQ:
tarjan一般用于找scc,然后把所有的scc都看作一个点,那么这张图就可以化简成一张弱联通图(或者压根不连通),一般作为一种化简技巧
这道题要确定给定的有向图每个点之间是否是都是可达的(对于u,v满足有路径可使u到v或者v到u)
那么首先想到对于每一个scc内部,显然满足这个条件,那么我们可以先用tarjan化简图,然后化简以后的图就是一个有向无环图了
那么不难想到如果最后要满足全部可达,最后的每一个scc一定要组成一条没有分叉的链即\(a_1->a_2->a_3....a_{n-1}->a_n\)这样,然后就是想一下如何判断是否是这样的链
如果直接dfs的话或者统计度并不行因为可能会有\(1->2,2->3,3->4,2->4\)这样的图存在最终导致dfs或者度的误判
这里我们采取拓扑排序的方式去解,因为拓扑排序不受上面这种前向边影响并且可以明确判断出分叉(分叉的时候会一次性入队两个导致队列大小大于1)
综上代码如下:

#include <bits/stdc++.h>
#include <iostream>
using namespace std;
using ll = long long;

const int N = 1e5 + 10;
struct E{int to,ne;}g[N << 1];
int h[N << 1], idx;
void add(int x, int y){g[idx].to = y, g[idx].ne = h[x], h[x] = idx ++;}
int dfn[N]{}, low[N]{}, T{};
int in_stk[N]{},stk[N]{}, tt = -1;
int col[N]{};
int scc_cnt{};
void tarjan(int u)
{
    dfn[u] = low[u] =  ++ T;
    stk[++ tt] = u;
    in_stk[u] = true;
    for(int i = h[u] ;~i; i = g[i].ne)
    {
        int v = g[i].to;
        if(!dfn[v])
        {
            tarjan(v);
            low[u] = min(low[v], low[u]);
        }
        else if(in_stk[v])
        {
            low[u] = min(low[u], dfn[v]);
        }
    }
    if(dfn[u] == low[u])//递归回退知道满足dfn[u] == low[u]此时点u就是以u根的最大scc的根
    {
        ++ scc_cnt;
        while(tt != -1 && stk[tt] != u)
        {
            int v = stk[tt -- ];
            in_stk[v] = false;
            col[v] = scc_cnt;
        }
        col[u] = scc_cnt;
        in_stk[u] = false;
        tt --;
    }
}
void solve()
{
	ll n, m;cin >> n >> m;
    memset(h, -1, sizeof h);
    for(int i =0 ; i < m ;i ++ )
    {
        int u, v;cin >> u >> v;
        add(u, v);
    }
    for(int i = 1 ;i<= n ;i ++ ) if(!dfn[i]) tarjan(i);
    vector<vector<int>>g0(n + 1);

    // cout << "TESt : " << col[1] << " " << col[2] << col[3]<< '\n';
    // cout << "TEST : " << scc_cnt << '\n';
    
    for(int u = 1 ; u <= n ; u ++ )
    {
        for(int j = h[u] ;~j; j = g[j].ne)
        {
            int v = g[j].to;
            if(col[u] == col[v]) continue;
            // cout << "TESt : " << u << " " << v << '\n';
            g0[col[u]].emplace_back(col[v]);
        }
    }
    vector<int>d(scc_cnt + 1, 0);
    for(int i = 1 ;i <= scc_cnt ;i ++ )
    {
        for(auto j : g0[i]) d[j] ++;
    }
    auto bfs = [&]()->bool
    {
        queue<int>q;
        for(int i = 1 ;i<= scc_cnt ; i ++ ) if(!d[i]) q.emplace(i);
        while(!q.empty())//不可能有环,所以不写vis数组了
        {
            if(q.size() > 1) return false;//只要在某一刻出现了两个起点(两条链就NO)
            int u = q.front();q.pop();
            for(int& v : g0[u])
            {
                d[v] --;
                if(!d[v]) q.emplace(v);
            }
        }
        return true;
    };
    if(bfs()) cout << "Yes\n";
    else cout << "No\n";
}
int main()
{
	cin.tie(0)->ios::sync_with_stdio(false);
	int _ = 1;
	while( _ -- ) solve();
	return 0;
}
posted @ 2026-06-17 17:00  Mikan_QWQ  阅读(5)  评论(0)    收藏  举报