[Codeforces] AIM Tech Round 5 (rated, Div. 1 + Div. 2) 总结+题解

Codeforces的这场是div1+div2的,理论上更容易上分,然而我C题被卡了...还好没掉太多分.个人感觉这一次比赛的出题人水平很高,很多题目看起来毫无思路,但是最终的解法都比较容易理解,没有用到复杂算法,非常有启发性,而且代码量都不是太大。这次比赛确实可以说是“题出的好!难度适中,覆盖知识点广,题目又着切合实际的背景,解法比较自然。给出题人点赞 !”这就是传说中的有良好区分度的题吧!(虽然我当场比赛的体验非常不好,凌晨比赛+卡题)于是今天把剩下的题目都刷了一下,写了这篇题解

A. Find Square

签到题,找出正方形的左上角和右下角顶点的位置,取平均值即可。关键代码(C++):

    read(r), read(c);
    rep(i, r) scanf("%s", s[i] + 1);
    int sx, sy, ex, ey;
    for(int i = 1; i <= r; ++i)
        for(int j = 1; j <= c; ++j)
            if(s[i][j] == 'B') {
                sx = i, sy = j; goto over;
            }
over: ;
    for(int i = r; i > 0; --i)
        for(int j = c; j > 0; --j)
            if(s[i][j] == 'B') {ex = i, ey = j;goto ed;}
ed: ;
    printf("%d %d\n", (sx+ex)/2, (sy + ey)/2);

B. Unnatual Conditions

思维题,即脑筋急转弯题。事实上可以很容易构造出两个数相加得到100000000000这种的,打印出来即可。(亏我想了10分钟)代码(Python):

print( '5' * 2230 )
print( '4' * 2229 +'5' )

C. Rectangles

我的思路是维护一个小根堆,里面保存考虑第i个矩形时所有可能的矩形,排序方式为出现次数的高低.具体做法是先将前两个矩形和他们的交(如果存在)加入优先队列,出现次数依次为1,1,2,再考虑把之后的矩形与优先队列中的矩形的交(如果存在)加入优先队列里,考虑完第i个矩形后弹出优先队列中出现次数为i-2和更小的矩形(因为这些矩形最多出现在n-2个矩形中,绝对不可能符合题意)。看起来很暴力,但是这样已经可以O(n^2)了,因为每一次优先队列最多只可能比前一次多一个元素,因此每次比较的次数也就是优先队列中元素个数,最多O(n)个,而一共n 个矩形,因此O(n^2).但这样通过这道题是不够的,事实上加一个非常微小的优化就可以把复杂度降到O(n):每次往优先队列中添加元素时检查重复,如果重复了,就直接修改已经存在的那个元素的优先级.这样做可以直接将优先队列中的元素个数降至O(1)

但是遗憾的是STL的优先队列不支持遍历每个元素,pb_ds会出奇怪的编译错误(要重载小括号运算符?)(我昨天就是被这些错误卡住了,没交上正解),可以使用make_heap或者手写堆.下面的代码是我后来实在没办法暴力复制队列元素的方法(因为队列元素个数常数级,所以即使暴力复制也只会增加常数,复杂度是不变的)140ms:

#include<bits/stdc++.h>
#define rep(i, n) for( int i = 1; i <= n; ++i )
using namespace std;
const int maxn = 142674;
int n;
struct rect{ // rectangle
    int lx, ly, rx, ry;
    rect(int x=0, int y=0, int z=0, int w=0):
        lx(x), ly(y), rx(z), ry(w){}
    bool operator == (const rect& rhs) const {
        return lx == rhs.lx && ly == rhs.ly && rx == rhs.rx && ry == rhs.ry;
    }
};
inline rect intersect(rect& r1, rect& r2){
    return rect(max(r1.lx, r2.lx ), max(r1.ly,r2.ly) ,min(r1.rx,r2.rx ), min(r1.ry,r2.ry));
}// 矩形的交
typedef pair<rect, int> pri;
pair<rect, int> q[10000];
signed main()
{
    scanf("%d", &n);
    int front = 0, rear = 0; // [front, rear)
    rep(i, n){
        int a,b,c,d;
        scanf("%d%d%d%d", &a, &b, &c, &d);
        rect me = rect(a, b, c, d);
        vector<pri> tobepush; //要加入数组的元素
        if (i <= 2) tobepush.push_back (make_pair(me, 1));
        for(register int j = rear - 1; j != front - 1; --j){
            //考虑将现在的矩形与数组中矩形的交加入数组
            rect inter = intersect(me, q[j].first);
            if( inter.lx <= inter.rx && inter.ly <= inter.ry ){//如果这个矩形存在
                bool ok = 1;
                for(vector<pri>::iterator it2 = tobepush.begin(); it2 != tobepush.end(); ++it2)
                //这个是判重,关键步骤,没有了会TLE
                    if(it2 -> first == inter) {
                        it2 -> second = max(it2->second, q[j].second + 1); ok = 0; break;
                }
                if(ok) tobepush.push_back(make_pair(inter, q[j].second + 1));
            }
        }
        int f = 0;
        for(int g = front; g != rear; ++g)
            if(q[g].second >= i - 1) q[f++] = q[g]; //暴力将数组重构
        front = 0, rear = f;
        for ( auto v : tobepush ) q[rear++] = v; //加入tobepush中的元素
    }
        printf("%d %d\n", q[front].first.lx, q[front].first.ly);
        //q[front].first就是一个满足条件的矩形,打印它的左下角就好了
	return 0;
}

D. Order Book

也是一道思维题吧..主要就是观察到令结果*2的是Accept而不是Add..维护一个保存当前元素的集合,需要考虑的元素的范围,以及在范围内的数的数量cnt.添加操作直接加入集合就行了,如果在范围内就增加计数器.每出现一个Accept,就把计数器清零,把元素删掉,找到it = lower_bound和--it (即这个元素的前驱后继)就可以断言大于*it的一定是SELL,小于*(--it)的一定是BUY ,不去考虑他们.因此更新需要考虑的范围.如果删掉的元素是需要考虑范围的边界,那Accept的元素是Buy是Sell已经确定,否则不确定,结果乘以二.这样就可以基本完成了,但是如果最后一次操作之后考虑范围内还有数怎么办?题目已经保证了Buy和Sell的价格有条件,因此这些数只有cnt+1种可能(即00...0,10...0, 11...0, ..., 11...1这些可能,0和1必须连续),最后乘以(cnt+1)即可.下面是代码(C++,296ms)

#include<bits/stdc++.h>
#define rep(i,n ) for(int i = 1; i <= n; ++i)
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;

template <typename T>
inline void read(T& x){
	int w = 1; T data = 0;
	char ch = getchar();
	while(!isdigit(ch) && ch != '-') ch = getchar();
	if(ch == '-') w = -1, ch = getchar();
	while(isdigit(ch))
		data = 10 * data + ch - '0', ch = getchar();
	x = data * w;
}
template <typename T>
void write(T x){
	 if (x < 0) putchar('-'),x = ~x + 1;
	 if (x > 9) write(x / 10);
	 putchar( x % 10 + '0');
}
template <typename T>
inline void writeln(T x){
	write(x), putchar('\n');
}
#define name(x) #x
#define PRINT(x) $ printf("%s = ", name(x)), cout << x << '\n'
#define max(a, b) (((a) > (b)) ? (a) : (b))
#define min(a, b) (((a) < (b)) ? (a) : (b))
//上面全是模板

constexpr ll mod = 1000000007LL, inf = 0x3fffffffffLL, minf = -inf;
ll ans = 1LL;
set<ll> s;
signed main()
{
    ll q, cnt = 1, lb = minf, ub = inf; read(q);
    while( q-- ){
        char opt[20]; ll p;
        scanf("%s", opt); read(p);
        if( opt[1] == 'D' ){
            s.insert(p);
            if( p >= lb && p <= ub ) cnt++;
        }
        else{
            if( p < lb || p > ub ) {puts("0"); return 0;}
            if( p != lb && p != ub ) ans = (ans << 1LL) % mod;
            s.erase(p);
            cnt = 1;
            auto it = s.lower_bound(p);
            if( it != s.end() ) ub = *it; else ub = inf;
            if( it != s.begin() ) lb = *(--it); else lb = -inf;
        }
    }
    writeln( ans * cnt % mod );
    return 0;
}

E. Restore Array

是一道构造题,但是坑点很多,我wa了3次re两次。。。可以证明如果序列元素全部相同且不为0,则一定无解,反之也成立。我们有以下的构造方法:先找一个起始位置pos。方法是这样的:找到序列的最大值,查找这个值出现的所有区域,有些区域最大值是连着出现的,我们称为二连块,而我们要找最右边的二连块,并取这个二连块中位于最左边的这个值。至于为什么取这个值,读者可以自己找找反例(我当时wa了几次)例如序列1,5,4,1,5,5,5,3,5,5,2中最大值是5,三条下划线划出了三个二连块,我们要取的5就是粗体倾斜的那一个。我们从起始位置开始向左走,维护途经的数的和,最后走回来(因为是环形)得到一个新序列。例如这个例子中,先走到3,产生3+5=8,再走到5,产生5+8=13,再走到5,产生5+13=18,以此类推。这个序列与标准答案已经很接近了,可以证明它可以满足大多数性质。例如上面的序列会产生34,33,28,24,23,18,13,8,5,39,是正确的。但是还是可以把它卡掉,例如0,0,0,0,1会产生1,1,1,1,1,但是1\mod1\neq 1,怎么办呢?实际上很简单,走第一步时多加一倍原数就好了,例如第一个例子中走到3时本应该产生3+5=8,但是这回我们让它变成3+5+5=13,其它的数按照原来的方法不变。这样后面的例子中产生的数列是2,2,2,2,1,符合要求。可以证明这种方法是正确的。代码:(C++,109ms)

#include<bits/stdc++.h>
using namespace std;
const int maxn = 150000;
long long a[maxn], b[maxn], n, mn = 0x3f3f3f3f, mx = 1, pos;
inline int pre(int x) {return x - 1 + ((x==1) ? n : 0);} //每个位置在环上的前驱
inline int nxt(int x) {return x + 1 - ((x==n) ? n : 0);} //每个位置在环上的后继
int main(){
    scanf("%lld", &n);
    bool allzero = 1;
    for(int i = 1; i <= n; ++i){
        scanf("%lld", a + i);
        if( a[i] >= mx && a[i] != a[pre(i)] )
            mx = a[i], pos = i;
        if( a[i] < mn ) mn = a[i];
        if( a[i] != 0 ) allzero = 0;
    }
    if( mn == mx || allzero ) { //数全都一样的情况
        if( allzero ) {
            puts("YES");
            for(int i = 1; i <= n; ++i)
                printf("1%c", i == n ? '\n' : ' ');
        }
        else puts("NO");
        return 0;
    }
    b[pos] = a[pos];
    int pp = pre(pos);
    b[pp] = a[pp] + b[pos] * 2; //第一个走到的数多加一次原数
    for(int i = pre(pp); i != pos; i = pre(i) )
        b[i] = a[i] + b[nxt(i)]; //维护途径的数的总和
    puts("YES");
    for(int i = 1; i <= n; ++i)
        printf("%lld%c", b[i], i==n ? '\n' : ' ');
    return 0;
}

F. Make Symmetrical

这道题脑洞很大,思路新奇,我非常喜欢。首先不难想到O(n^2)的做法,但是对于10^5太大了。最核心的观察是任何一个点经过对称变换之后和原来的点必定在同一个以原点为圆心的圆上,而每个点的坐标都是整数,每个圆上的整点个数实际上很少。事实上有如下定理:(对这个定理很感兴趣的同学强烈推荐3blue1brown的数学视频:隐藏在素数规律中的π,以及配套模板题BZOJ1041: [HAOI2008]圆上的整点

定理
圆上整点数量定理

因此我们只需要记录当前点的个数tot,维护每个半径对应的点组成的集合circ[r],以及对于某一斜率表示已配对的点的个数组成的映射ans。新加入点的时候,tot+=1,ans中这个点与原点所成的斜率的键值+=1,这个点与set中所有同半径的点都可以配对,对应的斜率为他们中点与原点所成的斜率,对这个斜率,令ans[k]+=2.最后将这个点加入circ[r]。删除点的时候恰恰相反就可以了。查询时只需要输出tot-ans[k]。代码如下:(3182ms,估计用pbds的gp_hash_table会好得多)

扫描二维码关注公众号,回复: 2992394 查看本文章
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, ll> pii;
map< ll, set<pii> > circ;
map<pii, ll> ans;
ll tot;
ll gcd( ll x, ll y ) {return y== 0 ? x :  gcd(y, x % y);}
inline pii elim( ll x, ll y ) { //细节:用pair<ll,ll>保存斜率
    ll gc = gcd( x , y );
    return make_pair( x / gc, y / gc);
}
inline ll d( ll x, ll y ) {return x * x + y * y;}
int main(){
    int q; scanf("%d", &q);
    while( q-- ){
        int opt, x, y;
        scanf("%d%d%d", &opt, &x, &y);
        if( opt == 1 ){
            tot++;
            ans[elim( x, y )] += 1;
            for ( auto v : circ[d(x, y)] )
                ans[elim( v.first + x, v.second + y )] += 2;
            circ[d(x, y)].insert({x, y});
        } else if ( opt == 2 ){
            tot--;
            ans[elim( x, y )] -= 1;
            circ[d( x, y )]. erase({x, y});
            for( auto v : circ[d( x, y )])
                ans[elim(v.first + x, v.second + y)] -= 2;
        } else{
            printf("%lld\n", tot - ans[elim(x, y)]);
        }
    }
}

G. Guess the Number

H. Make Square

猜你喜欢

转载自blog.csdn.net/qq_41255467/article/details/82152457