近々、AtCoderの言語アップデートがあり、.net 7から.net 9になります。
.net 9の機能を使った、標準入力をパースするのに便利な拡張を紹介します。
public static class StringExtension
{
public static (T1, T2) SplitAs<T1, T2>(this string s)
where T1 : ISpanParsable<T1>
where T2 : ISpanParsable<T2>
{
var chars = s.AsSpan();
Span<Range> ranges = stackalloc Range[2];
var cnt = chars.Split(ranges, ' ');
T1 val1 = T1.Parse(chars[ranges[0]], null);
T2 val2 = T2.Parse(chars[ranges[1]], null);
return (val1, val2);
}
public static List<T> SplitList<T>(this string s, int capacity = -1) where T : ISpanParsable<T>
{
var result = capacity == -1 ? new List<T>() : new List<T>(capacity);
var span = s.AsSpan();
foreach(var range in span.Split(' '))
{
result.Add(T.Parse(span[range], null));
}
return result;
}
}
たとえば次のように使います。
//2つの数を受け取る
//before
var chunks = Console.ReadLine().Split(' ').Select(int.Parse);
var N = chunks[0];
var M = chunks[1];
//after
var (N, M) = Console.Readline().SplitAs<int, int>();
//数字のリストを受け取る
//before
var list = Console.ReadLine().Split(' ').Select(int.Parse).ToList()
//after
var list = Console.ReadLine().SplitList<int>();
//クエリを受け取る
//before
var chunks = Console.ReadLine().Split(' ');
var type = int.Parse(chunks[0]);
var query = chunks[1]
//after
var (type, query) = Console.ReadLine().SplitAs<int, string>();
varで書けるところが気に入っています。
Program.csにぺたりするか、ライブラリ化してSourceExpanderを使うと便利です。
SplitAsは、項が3つとか4つのものも同じ要領で作れます。
おまけ
本編は以上ですが、速度にすこしこだわっているので、こだわりポイントの紹介をします。
一応、素朴に書くより速度は良いはずですが、これでTLEが解決する類のものではありません。単なるヘルパーとお考えください。
ReadOnlySpan<char>.Split(Span<Range> destination)
.NET8の新機能です。
string.Split()はstringの配列を生成しますが、intやlongで受け取りたい場合はこれがやや冗長です。
これはReadOnlySpanを解析し、Rangeのみを返します。ReadOnlySpan<char>で文字列を切り出し、ISpanParsable.Parseに突っ込むことで、stringを経由する必要がなくなります。
あらかじめ Span<Range> を用意しておく必要があります。
ReadOnlySpan<T>.Split(T separator)
SplitListで使ってるやつです。.NET9の新機能です。
上のやつに似ていますが、こちらはforeachでRangeを順次受け取れます。要素数が不明な場合にも使えるのがポイントです。
new List<T>(int capacity)
C#のListは要素数可変ですが、内部実装では配列を持ち、動的に拡張を行っています。要素数がわかっているなら生成時に指定することで再確保を行わなくなり、パフォーマンスが良いです。
ぶっちゃけ要素数固定なら配列のほうがパフォーマンスはいいです
なお、グラフみたいに、大量のListを生成し、かつ各要素数がわからない場合、考えうる最大値をcapacityとして指定してしまうと、逆にパフォーマンスが低下するだけでなく、MLEを起こす可能性があります。この場合は逆に指定しないほうがよいです。
stackalloc
特に新機能ではありませんが、スタック上で配列を扱う機能です。GCの負担がありません。構造体の小さい配列で、寿命が短いときに適しています。
ISpanParsable<T>
ReadOnlySpan<char> からTへの変換を提供するインターフェースです。
これ自体は.net 7からありましたが、string非対応だったため使いづらいところがありました。
.net 8からstringにも実装され、かゆいところに手が届くようになりました。
おまけのおまけ
.NET7での実装も参考までに載せておきます。
public static class StringExtension
{
public static (T1, T2) SplitAs<T1, T2>(this string s)
{
var chars = s.AsSpan();
for (var i = 0; i < chars.Length; i++)
{
if (chars[i] == ' ')
{
T1 val1 = ParseOrString<T1>(chars.Slice(0, i));
T2 val2 = ParseOrString<T2>(chars.Slice(i + 1));
return (val1, val2);
}
}
throw new Exception();
}
private static T ParseOrString<T>(ReadOnlySpan<char> chars)
{
var t = typeof(T);
if (t == typeof(int))
{
return (T)(object)int.Parse(chars);
}
if (t == typeof(long))
{
return (T)(object)long.Parse(chars);
}
if (t == typeof(string))
{
return (T)(object)new string(chars);
}
//...doubleとかdecimalとか必要な分だけ書く
throw new InvalidCastException("");
}
public List<T> Split<T>(string s, int capacity = -1) where T : ISpanParsable<T>
{
var result = capacity >= 0 ? new List<T>(capacity) : new List<T>();
var chars = s.AsSpan();
var start = 0;
for (var i = 0; i < chars.Length; i++)
{
if (chars[i] == ' ')
{
result.Add(T.Parse(chars.Slice(start, i - start), null));
start = i + 1;
}
}
if (start < chars.Length)
{
result.Add(T.Parse(chars.Slice(start), null));
}
return result;
}
}
JIT最適化を当てにしているところがあり、ISpanParasableのありがたみがわかります。