2025牛客多校第十场 K.神奇集合 F.老师和Yuuka逛商场 E.老师与好感度 I.矩阵 个人题解

I.矩阵

数学 #贪心 #构造

题目

思路

首先考虑有数最受条件的约束,因此尝试令数\(x\)沿着某方向前进\(x\)后回到原地:

\[\begin{align} (x+x+1)\%n-1&=x\\ \\ (2x+1)\%n&=x+1\\ \\ 2x+1&\equiv x+1\mod n\\ \\ x&\equiv 0\mod n \end{align} \]

则有\(x\)为\(n\)的因数

因此,当\(x\)为\(n\)的因数时,\(x\)无法在\(n\)方向上进行移动,\(m\)方向同理

因此,\(x=lcm(n,m)\)时,\(x\)一定无法移动

因此\(lcm(n,m)\)必须为最后一个填入的数字,可以利用这一点进行\(YES/NO\)的判断

接下来通过观察贪心地进行填空:

  • 尝试将一列填满后再填下一列
  • 尝试每填完一个数就变换一次移动方向,比如这次在列方向上向下移动,下一次列方向上的移动就向上,这样可以保证移动后不会踩到已经被填过的格子

代码实现

代码由\(phaethon 90\)书写

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;
#define ll long long
#define endl '\n'

ll gcd(ll a,ll b)
{
    if(b==0) return a;
    else return gcd(b,a%b);
}
void eachT()
{
    ll n,m;
    cin>>n>>m;
    if(n/gcd(n,m)*m<n*m)
    {
        cout<<"NO\n";
        return;
    }

    vector<vector<int>>mp(n+1,vector<int>(m+1,0));
    int i=0,j=0,cnt=0;
    int dirn=1,dirm=1;
    mp[0][0] = ++cnt;
    while(cnt<n*m)
    {
        if(cnt%n==0)
        {
            j = (j+cnt%m*dirm+m*m)%m;
            mp[i][j] = ++cnt;
            dirm *= -1;
        }
        else
        {
            i = (i+cnt%n*dirn+n*n)%n;
            mp[i][j] = ++cnt;
            dirn *= -1;
        }
    }
    cout<<"YES\n";
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<m;j++)
        {
            cout<<mp[i][j]<<' ';
        }
        cout<<endl;
    }
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);

    int t=1;
    // cin>>t;
    while(t--) eachT();
    return 0;
}

E.老师与好感度

dp #线性dp

题目

思路

由于\(0\leq a_{i}\leq 100\),很容易想到要枚举最后出现的\(m\)个数,考虑\(m=2\)的情况,即枚举两个目标\(tar_{1},tar_{2}\)(\(target\))

状态表示:
\(dp[i][j]\)表示从\(1\)遍历到\(i\),第\(i\)个学生的好感度变为\(tar_{j}\)(\(1\leq j\leq 2\))的最小总天数

状态转移:

\[\begin{align} &dp[i][j]=\min_{0\leq k\leq[m=2]}\{dp[i][j]\ , \ dp[i-1][k]+ \max\{ tar_{j}-a_{i}-(tar_{k}-a_{i-1})\ ,\ 0 \} \},0\leq j\leq[m=2]\\ \\ &if(tar_{j}-a_{i}<0)dp[i][j]=inf \end{align} \]

当\(m=2\)的时候,\(dp[i][j]\)将由\(i-1\)时的两个状态转移过来,分别是选\(tar_{1},tar_{2}\)的状态

要与\(0\)取\(max\)是因为\(a_{i}\)与\(tar\)的差距可能比较小,仅用之前的天数就可以达到目标

枚举\(tar_{1},tar_{2}\),每次都取全局最小值即可

注意,\(tar_{1}\)的上下界分别为\(a_{i}\)的最大值与最小值,\(tar_{2}\)的上下界分别为\(200\)与\(tar_{1}\)

\(tar_{2}\)的上界不是\(a_{i}\)的最大值,因为可能会有区间覆盖在\(a_{i}\)最大值上使得它变大

最坏情况下,\(a_{i}\)的最大值被覆盖的次数量级为\(o(n)\),因此上界设为\(100+100\)即可

代码实现

cpp 复制代码
#include<iostream>
#include<vector>
#include<map>
#include<cmath>
#include<set>
using namespace std;
using ll = long long;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define see(stl) for(auto&ele:stl)cout<<ele<<" "; cout<<'\n';
constexpr int inf = 1e9 + 5;
// #define int ll 

const int up=155;
void chmin(int&x,int y){
    x=min(x,y);
}
void chmax(int&x,int y){
    x=max(x,y);
}

const int N=405;
int a[N],dp[N][2],t[2];

void eachT() {   
    int n,m;cin>>n>>m;
    int ma=0,mi=inf;
    rep(i,1,n){
        cin>>a[i];
        chmax(ma,a[i]);
        chmin(mi,a[i]);
        dp[i][0]=dp[i][1]=inf;
    }
    int ans=inf;
    rep(tar1,mi,ma){
        rep(tar2,tar1,up){
            t[0]=tar1,t[1]=tar2;
            dp[1][0]=t[0]-a[1];
            dp[1][1]=(m==2)?t[1]-a[1]:inf;  
            if(dp[1][0]<0)dp[1][0]=inf;
            if(dp[1][1]<0)dp[1][1]=inf;
            rep(i,2,n){
                rep(j,0,(m==2)){
                    rep(k,0,(m==2)){
                        chmin(dp[i][j],max(t[j]-a[i]-(t[k]-a[i-1]),0)+dp[i-1][k]);
                        if(t[j]-a[i]<0)dp[i][j]=inf;
                    }
                }            
            }  
            chmin(ans,min(dp[n][0],dp[n][1]));
            rep(i,2,n)dp[i][0]=dp[i][1]=inf;
        }
    }
    cout<<ans<<'\n';
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    ll t = 1;
    cin >> t;
    while (t--) {
        eachT();
    }
}

F. 老师和 Yuuka 逛商场

线段树 #线段树二分

题目

思路

由于要确定两个隔板,非常容易想到的一个思路便是\(o(n)\)遍历左隔板,\(o(\log n)\)查找最优右隔板,总复杂度\(o(n\log n)\)

设集合\(S_{l}\)表示区间\([1,l]\)内的元素(去重),那么\([l+1,n]\)内能对答案有贡献的数字必然要属于\(S_{l}\),因此我们可以将\([l+1,n]\)这一段序列视作只剩下了属于\(S_{l}\)的元素

我们将序列分为三段:\([1,l],[l+1,r-1],[r,n]\)

目标即使得\([l+1,r-1],[r,n]\)中共同元素的数量尽可能大

因此我们对每个元素在区间\([l+1,n]\)中最早出现和最晚出现的位置\(lpos,rpos\)进行维护,当指针落于元素\(x\)的\([lpos,rpos]\)中时便说明该指针的左边与右边必定都有元素\(x\)出现

每次移动\(l\)指针的时候,都使用队列\(deque\)更新元素\(a[l]\)的\(lpos,rpos\)

接着,使用线段树对区间\([lpos+1,rpos]\)进行区间\(+1\),同时维护区间的最大值

为什么左端点是\(lpos+1\)呢?

这是为了保证指针\(r\)坐落于区间中的时候,\([r,n]\)上必定有元素\(x\),\([l+1,r-1]\)上也必定有元素\(x\)

考虑边缘情况,若\(r=lpos+1\),那么\(r-1=lpos\),刚好将最左边的元素\(x\)包含进了区间中

令\(r\)指针落在区间\([l+1,n]\)的\(max\)值的点上,此时左右区间必定有\(max\)值个相同元素,此处便是最优分割处

如何在线段树上找到这个\(max\)值的位置呢?

我们可以在线段树上二分,通过\(pair\)来储存\(max\)的值与位置:

  • \(pair\!<\!int,int\!>\)类型的\(find(p,l,r)\)函数

    • 传回的\(pair\)中,\(first\)为\(max\)的值,\(second\)为\(max\)值的位置
    • 功能:查询区间\([l,r]\)内的单点最大值及其位置
    • 对比左右子树的\(max\)值,选择\(max\)值较大的子树分裂查询
  • \(pair\!<\!int,int\!>\)类型的\(query(p,l,r,ql,qr)\)函数

    • 传回的\(pair\)中,\(first\)为\(max\)的值,\(second\)为\(max\)值的位置
    • 功能:查询区间\([ql,qr]\)内的单点最大值及其位置
    • 若\([l,r]\)被\([ql,qr]\)完全覆盖,那么可以直接调用\(find(p,l,r)\)
    • 否则需要分裂查询左子树与右子树,返回左右子树中的较大者

为什么要设计两个函数来二分查找?

因为给定的\(ql,qr\)不一定是某个已知线段树节点所维护的区间\(l,r\),可能需要分裂成多个区间进行比较

通过\(query\)函数将\([ql,qr]\)分裂成一个个已知区间\([l,r]\),再通过\(find\)函数在线段树上查找

每次更新\(l\)指针时,先删去\(a[l]\)的\([lpos+1,rpos]\),\(a[l]\)的\(deque\)进行\(pop\_front\)之后再加回\([lpos'+1,rpos]\)

调用\(query(1,1,n,l+2,n)\)查询区间\([l+2,n]\)中的最大值与位置以确定最优的\(r\)指针

之所以是\(l+2\)的原因是,当\(r\)取\(l+2\)的时候,\(r-1\)为\(l+1\),与之前讨论的边界情况相符

注意\(l,r\)分别初始化为\(2,3\),以防\(n=3\)的情况

一直取全局最大值即可

代码实现

cpp 复制代码
#include<iostream>
#include<vector>
#include<cmath>
#include<queue>
#include<unordered_set>
#include<unordered_map>
using namespace std;
using ll = long long;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define see(stl) for(auto&ele:stl)cout<<ele<<" "; cout<<'\n';
constexpr int inf = 1e9;
#define int ll 
// #define double long double

const int N = 1e5 + 5e4 + 5;
int n, a[N];

unordered_map<int, deque<int>>mp;

#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)

int add[N << 2], ma[N << 2];
void pushup(int p) {
    ma[p] = max(ma[ls], ma[rs]);
}
void pushdown(int p, int l, int r) {
    if (add[p]) {
        ma[ls] += add[p];
        ma[rs] += add[p];
        add[ls] += add[p];
        add[rs] += add[p];
        add[p] = 0;
    }
}
void modify(int p, int l, int r, int x, int y, int val) {
    if (x <= l && r <= y) { ma[p] += val; add[p] += val; return; }
    pushdown(p, l, r);
    if (x <= mid)modify(ls, l, mid, x, y, val);
    if (y > mid)modify(rs, mid + 1, r, x, y, val);
    pushup(p);
}

pair<int, int> find(int p, int l, int r) {
    if (l == r)return { ma[p],l };
    pushdown(p, l, r);
    if (ma[ls] >= ma[rs])return find(ls, l, mid);
    else return find(rs, mid + 1, r);
}

pair<int, int> query(int p, int l, int r, int ql, int qr) {
    if (ql > r || qr < l) return { -1,-1 }; 
    if (ql <= l && r <= qr) return find(p, l, r);
    pushdown(p, l, r);
    auto left = query(ls, l, mid, ql, qr);
    auto right = query(rs, mid + 1, r, ql, qr);
    return left.first >= right.first ? left : right;
}

void build(int p, int l, int r) {
    ma[p] = add[p] = 0;
    if (l == r)return;
    build(ls, l, mid); build(rs, mid + 1, r);
}

void eachT() {
    cin >> n;

    mp.clear();
    build(1, 1, n);

    unordered_multiset<int>sl;
    rep(i, 1, n) {
        cin >> a[i];
        mp[a[i]].push_back(i);
    }
    int ans = 0, ansl = 2, ansr = 3;
    rep(l, 1, n) {
        if (!sl.count(a[l])) {
            mp[a[l]].pop_front();
            int posl = mp[a[l]].front(), posr = mp[a[l]].back();
            if (posl + 1 <= posr)modify(1, 1, n, posl + 1, posr, 1);
        } else {
            int posl = mp[a[l]].front(), posr = mp[a[l]].back();
            if (posl + 1 <= posr)modify(1, 1, n, posl + 1, posr, -1);
            mp[a[l]].pop_front();
            if (mp[a[l]].size() >= 2) {
                int posl = mp[a[l]].front(), posr = mp[a[l]].back();
                if (posl + 1 <= posr)modify(1, 1, n, posl + 1, posr, 1);
            }
        }
        sl.insert(a[l]);
        pair<int, int> now = query(1, 1, n, l + 2, n);
        if (ans < now.first) {
            ans = now.first;
            ansl = l + 1, ansr = now.second;
        }
    }
    cout << ans << '\n' << ansl << " " << ansr << '\n';
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    ll t = 1;
    cin >> t;
    while (t--) {
        eachT();
    }
}

K. 神奇集合

强联通分量 #树上背包 #dp

题目

思路

注意到题目中添加的新边为返祖边 ,一旦选择了该连通块中的任意一个点,那么这整个连通块都必须要选择,因此考虑对图进行\(tarjan\)缩点

缩点过程中,将新点的权值、入度进行记录

由于原图是树,则添加返祖边缩点后仍然是一棵树

建好新图后遍历所有新节点,找出入度为0的点记为根节点

从根节点进入,\(dfs\)过程中进行树上背包dp:

状态表示:
\(dp[u][j]=1 /0\)代表以\(u\)为根的子树中,是否存在总权值为\(j\)的神奇集合

状态转移:

\[\begin{align} &sum_{u}\geq i\geq 0,sum_{son}\geq j\geq 0:dp[u][i+j]\ |=dp[u][i]\&dp[son][j]\\ \\ &dp[u][sum_{u}]=1 \end{align} \]

从\(u\)节点已经遍历的所有可能权值中取出权值为\(i\)的情况,从\(son\)节点的所有可能权值中取出权值为\(j\)的情况,二者可以转移到\(dp[u][i+j]\)的状态中

最后考虑整个子树都选上的情况\(dp[u][sum_{u}]\)

答案即为\(\sum_{i=0}^{N} dp[root][i]\)

代码实现

cpp 复制代码
#include<iostream>
#include<vector>
#include<map>
#include<cmath>
#include<set>
#include<stack>
#include<unordered_map>
using namespace std;
using ll = long long;
#define int ll 
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define see(stl) for(auto&ele:stl)cout<<ele<<" "; cout<<'\n';
constexpr int inf = 1e9 + 5;

// #include <ext/pb_ds/assoc_container.hpp>
// using namespace __gnu_pbds;
const int N=1e4+5;
int w[N];

struct node{
    set<int>e;
    int dfn,low,in,scc;
}a[N];

struct node1{
    set<int>e;
    int siz,deg,w,sum;
}na[N];

int tot,cnt;
stack<int>st;
void tarjan(int u){
    a[u].dfn=a[u].low=++tot;
    st.push(u),a[u].in=1;
    for(auto&son:a[u].e){
        if(!a[son].dfn){
            tarjan(son);
            a[u].low=min(a[u].low,a[son].low);
        }else if(a[son].in){
            a[u].low=min(a[u].low,a[son].low);
        }
    }
    if(a[u].dfn==a[u].low){
        int v;++cnt;
        do{
            v=st.top(),st.pop(),a[v].in=0;
            a[v].scc=cnt,++na[cnt].siz;
            na[cnt].w+=w[v];
        }while(u!=v);
    }
}

int n;
void build(){
    rep(i,1,n){
        for(auto&son:a[i].e){
            if(a[i].scc!=a[son].scc){
                na[a[i].scc].e.insert(a[son].scc);
                na[a[son].scc].deg++;
            }
        }
    }
}

bool dp[N][N];

int dfs(int u,int fa){
    int sum=0;
    dp[u][0]=1;
    for(auto&son:na[u].e){
        if(son==fa)continue;
        sum+=dfs(son,u);
        per(i,na[u].sum,0){
            per(j,na[son].sum,0){
                dp[u][i+j]|=dp[u][i]&dp[son][j];
            }
        }
        na[u].sum=sum;
    }
    sum+=na[u].w;
    na[u].sum=sum;
    dp[u][sum]=1;
    return sum;
}

void eachT() {   
    cin>>n;
    rep(i,1,n)cin>>w[i];
    rep(i,1,n-1){
        int u,v;cin>>u>>v;
        a[u].e.insert(v);
    }
    int m;cin>>m;
    rep(i,1,m){
        int u,v;cin>>u>>v;
        if(u!=v)a[u].e.insert(v);
    }
    rep(i,1,n){
        if(!a[i].scc)tarjan(i);
    }
    build();
    int rt;
    rep(i,1,cnt){
        if(na[i].deg==0)rt=i;
    }
    dfs(rt,0);
    int cnt=0;
    rep(i,0,N-1)if(dp[rt][i])cnt++;
    cout<<cnt<<'\n';
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    ll t = 1;
    // cin >> t;
    while (t--) {
        eachT();
    }
}
相关推荐
databook1 天前
把数学对象画出来:Manim Mobject类库速查手册
python·数学·动效
CUC-MenG2 天前
2025牛客多校第九场 G.排列 A.AVL树 F.军训 个人题解
数学·dfs·dp·笛卡尔树·组合数·曼哈顿距离·树上dp
Always_away2 天前
数学分析| 极限论| 1.数列极限常用方法总结
笔记·学习·考研·数学
CUC-MenG4 天前
2025杭电多校第八场 最有节目效果的一集、最自律的松鼠、最甜的小情侣、最努力的活着 个人题解
数学·线段树·高精度·模拟·dp·红黑树·线性dp·平衡树·线段树维护矩阵
CUC-MenG5 天前
2025杭电多校第七场 矩形框选、伤害冷却比 个人题解
数学·线段树·差分·扫描线·二维数点·区间最值
CUC-MenG6 天前
2025牛客多校第八场 根号-2进制 个人题解
数学·fft
CUC-MenG6 天前
2025牛客多校第七场 双生、象牙 个人题解
数学·gcd·欧拉筛·质数筛
zhmc7 天前
欧拉公式的意义
数学·应用数学
CUC-MenG11 天前
快速莫比乌斯变换(FMT)与莫比乌斯反演 例题:树上lcm
数学·dfs·fmt