Codeforces Round #634 (Div. 3)

Codeforces Round #634 (Div. 3)

A B C
没啥好说的

D
稍微卡了会,怕耽误时间就跳着去看了下E和F;之后发现过的人还是蛮多的,就开始回头做这个题。

最开始做的时候没有什么思路,后来考虑先把九宫格内满足anti条件,可以想到,每一个九宫格至少将一个数变成其他的数,才有可能满足anti条件;而题目约束至多只能改变9个数,那么显然一夹逼,就可以从这里下手进行进一步的处理。再多想一步,会发现每一个九宫格都要改变一次其实也就对应着每一行至少要改变一次,每一列至少要改变一次的情况。而又因为在一行/列/九宫格内只要将一个数变成其他数,就一定会出现2个相同数,所以我们剩余要做的就是将9个欲修改的数进行合理分布,使得每一行每一列每一个九宫格都仅(更多数影响也是可以的,不过仅有一个数的构造更加方便简单)受到一个数的影响,这样就可以保证anti条件。

那么随便构造一个类"九皇后"就行了。

E
直接看了E2…因为实在不想想两个算法。
这道题思路其实比较显然…枚举一个位置,快速计算另一个位置并计数…然而我大部分时间都花在了选择什么STL容器和怎么开容器并计算时空复杂度了…

看了下tutorial的E2,大体思路类似,不过都是 O ( n ∗ 200 ) O(n*200) O(n200)的,不知道有没有 O ( n l o g n ) O(nlogn) O(nlogn)的。

在三元回文串中,最开始考虑枚举所有可能的字母,然后一个个枚举第二个子串终点的位置,利用预处理的结果快速得到该位置之后所可以获得的该字母的最多个数,利用这个个数再想个办法快速获得第一个子串的终点位置,最后再利用这两个终点位置 O ( 200 ) O(200) O(200)的查询中间可能的最多个数。

然而这样应该会TLE。所以换了个思路,枚举每个字母可能的前后缀的个数,再利用预处理的结果直接得到这两个区间的位置,最后 O ( 200 ) O(200) O(200)的查询中间的最大个数。这样做的时间复杂度不大,原因在于总共2e5的个数已经被限制住了,如果某个字母出现的次数较多,那么其余字母出现的次数自然就会变小,这样就比之前枚举每个下标要好上很多。

测了下空间,最大数组2e7大概是170MB < 256MB,也差不多可以,交上去之后的空间也确实差不多这么大。

快乐1TRY

const int maxn = 2e5 + 10;
const int N = 200 + 10;
set<int> s;

//从1开始编号
int a[maxn], pre[N][maxn];
//从0开始编号
vector<int> L[N], R[N];

//查找[l, r]中出现最多的子母。
int query(int l, int r){
    if(l > r) return 0;
    int ans = 0;
    for(auto x: s){
        ans = max(ans, pre[x][r] - pre[x][l - 1]);
    }
    return ans;
}

void test(){
    memset(a, -1, sizeof a);
    memset(pre, -1, sizeof pre);
}

int main(){
//    Fast;
    int T; scanf("%d", &T); while(T--){
        s.clear();
        int n; scanf("%d", &n);
        for(int i = 1; i <= n; i++){
            scanf("%d", a + i);
            L[a[i]].push_back(i);
            pre[a[i]][i] = 1;
            s.insert(a[i]);
        }
        for(int i = n; i >= 1; i--) R[a[i]].push_back(i);
        for(auto x: s) for(int i = 1; i <= n; i++)
            pre[x][i] += pre[x][i - 1];

        //考虑左右两边是x的情况
        int ans = 1;
        for(auto x: s){
            int size = (int)L[x].size();
            //i = 0, .. size - 1; i + 1是实际出现的次数。
            for(int i = 0; i < size; i++){
                int left = L[x][i], right = R[x][i];
                //必须严格小于!
                if(left >= right) break;
                ans = max(ans, 2 * (i + 1) + query(left + 1, right - 1));
            }
        }
        printf("%d\n", ans);

        for(auto x: s){
            L[x].clear(); R[x].clear();
            for(int i = 1; i <= n; i++) pre[x][i] = 0;
        }
    }
    return 0;
}

F
并没有做出来。晚上考虑的是在一个循环圆中,回转一次就可以将一个白点倒回去,如果若干次的回转后不影响最终的满循环,那么此时如果有更多的黑点就应该取这种情况。尽管昨晚发现这样处理非常复杂,而且不易操作,考虑到了要从另一个角度,但是脑力不足,想不到正确的做法。

今天学习了题解,妙哉!
我考虑的是从终点状态往前转移,而题解的做法是从开始的状态往后转移。对于某一个循环圆和它的所有树枝,当经过了充分长的时间T后,所有可能的点都会在这个圆上做满循环;而在起始状态下,对于每一个格点也考虑它运动了T时间后所在的位置:如果有两个点在T运动之后的最终位置相重合,由于所有点运动的同时性,这两个点显然就不能同时取。我们最终取的点只能是那些经过T时间能到达该位置的的其中 一个点;而如果这些可能的点中有一个黑点,那么取这个黑点就可以使得最终黑点尽可能的多!

事实上,这个图的每一个格点的方向均确定,所以我们可以称它为一个 函数图(functional graph)。我们在上述做的事情其实就是给定运动时间T,找每个点经过T时间运动后的映射;因为最终的T时间映射结果已知(循环圆),所以我们要做的就是对每一个映射结果找一个最优的原象(本题中就是尽可能找黑点)!我昨晚尝试做的思路是从映射结果推原象,而这里的做法就是从原象推映射。 此外,T的取值具体是多少无关紧要,只要满足充分大的条件即可;这是由循环圆的循环特征所决定的。

以上讨论的是函数图相关理解,回到本题的话问题就很容易了。因为图最大为 n m nm nm,所以取时间 T = n m T = nm T=nm一定能让每个点都进入到最终循环中;如何快速求解每个点经过T时间后的映射结果呢?倍增手法(binary lifting) 很值得学习!设置数组nex[pos][wei]表示一维位置pos经过 2 w e i 2^{wei} 2wei次可以到达的点的一维位置,状态转移方程容易得到:
n e x [ p o s ] [ w e i ] = n e x [ n e x [ p o s ] [ w e i − 1 ] ] [ w e i − 1 ] nex[pos][wei] = nex[nex[pos][wei - 1]][wei - 1] nex[pos][wei]=nex[nex[pos][wei1]][wei1]
用相邻位置的移动来初始化wei == 0的情况即可。

之后随便找一下原象就行了。

代码

const int maxn = 1e6 + 10;

int n, m;
vector<string> vec, c;

int log2(int n){
    int wei = 0, res = 1;
    while(res < n){
        res *= 2; wei++;
    }
    return wei;
}

int nex[maxn][21];
set<int> s1, s2;

void ini(){
    vec.clear(); c.clear();
    s1.clear(); s2.clear();
}

int main(){
    ios::sync_with_stdio(false);
    
//    Fast;
    string temp;
    int T; cin >> T; while(T--){
        cin >> n >> m;
        ini();
        for(int i = 0; i < n; i++){ cin >> temp; c.push_back(temp);}
        for(int i = 0; i < n; i++){ cin >> temp; vec.push_back(temp);}
        
        n *= m;
        
        int W = log2(n);
        for(int pos = 0; pos < n; pos++){
            int x = pos / m, y = pos % m;
            switch(vec[x][y]){
                case 'L': nex[pos][0] = pos - 1; break;
                case 'R': nex[pos][0] = pos + 1; break;
                case 'U': nex[pos][0] = pos - m; break;
                case 'D': nex[pos][0] = pos + m; break;
            }
        }
        for(int wei = 1; wei <= W; wei++){
            for(int pos = 0; pos < n; pos++){
                nex[pos][wei] = nex[nex[pos][wei - 1]][wei - 1];
            }
        }
        
        for(int i = 0; i < n; i++){
            s1.insert(nex[i][W]);
            if(c[i / m][i % m] == '0') s2.insert(nex[i][W]);
        }
        cout << (int)s1.size() << ' ' << (int)s2.size() << '\n';
    }
    return 0;
}

坑点
因为string类的读入导致的cin相关问题(要我这个手打堆排的C选手的命了):

在这里插入图片描述

第一第二个AC是关闭stdio和iosteam的同步后,仅仅用cin和cout读写,分别tie(0)和不tie(0)的结果,发现运行时间并没有很大区别。

后面的WA基本都是用了cin读入string,也混用过了printf/scanf并且关闭了同步的结果。

再往后的AC是不关闭同步的结果,此时可以混用两个库的读写,然而时间开销直接翻倍。

⚠️所以综上考虑,以后要是遇到不得不用iosteam库的情况,为了不被卡TLE,必须使用语句ios::sync_with_stdio(false)来关闭同步,并且整个程序中不允许出现<cstdio>库的相关函数!cin.tie(0)就无需浪费时间去敲这一行代码了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值