unittest
{
	assert( [ "3", "1", "2 1", "1", "1 1", "1", "2 0" ].parse.expand.solve == 2 );
	assert( [ "3", "2", "2 1", "3 0", "2", "3 1", "1 0", "2", "1 1", "2 0" ].parse.expand.solve == 0 );
	assert( [ "2", "1", "2 0", "1", "1 0" ].parse.expand.solve == 1 );
}



import core.bitop;
import std.algorithm;
import std.conv;
import std.range;
import std.stdio;
import std.typecons;

void main()
{
	stdin.byLineCopy.parse.expand.solve.writeln;
}

auto parse( Range )( Range input )
if( isInputRange!Range && is( ElementType!Range == string ) )
{
	auto n = input.front.to!( long );
	auto xyss = new long[ 2 ][][ n ];
	foreach( ref xys; xyss )
	{
		input.popFront;
		auto a = input.front.to!( long );
		xys.length = a;
		foreach( ref xy; xys )
		{
			input.popFront;
			xy = input.front.split.to!( long[] );
			xy[ 0 ]--;
		}
	}
	return tuple( xyss );
}

auto solve( long[ 2 ][][] xyss )
{
	// 正直者と不親切な人のパターンを全検索
	auto ans = 0L;
	LOOP: for( auto b = 0L; b < ( 1L << xyss.length ); b++ )
	{
		foreach( i, xys; xyss )
		{
			// 不親切な人と仮定したの証言はスキップ
			if( ( b & ( 1L << i ) ) == 0 ) continue;
			
			// 証言が正しいか判定
			foreach( xy; xys )
			{
				if( xy[ 1 ] == 0 )
				{
					if( ( b & ( 1L << xy[ 0 ] ) ) != 0 ) continue LOOP;
				}
				else
				{
					if( ( b & ( 1L << xy[ 0 ] ) ) == 0 ) continue LOOP;
				}
			}
		}
		
		// 証言が一致したので答えを更新
		ans = max( ans, b._popcnt );
	}
	return ans;
}
